Ekwo OS — open source accounting: ledger, invoices and VAT on your own Supabase or Postgres
UnexploredDouble-entry books an agent keeps as the user: invoices, VAT returns, 107 country packs.
Install
Terminal
$npx -y @ekwo-ai/mcpmcp_config.json
{
"mcpServers": {
"ai-ekwo-mcp": {
"env": {
"EKWO_EMAIL": "${EKWO_EMAIL}",
"EKWO_DB_URL": "${EKWO_DB_URL}",
"SUPABASE_URL": "${SUPABASE_URL}",
"EKWO_PASSWORD": "${EKWO_PASSWORD}",
"EKWO_ACCESS_TOKEN": "${EKWO_ACCESS_TOKEN}",
"SUPABASE_ANON_KEY": "${SUPABASE_ANON_KEY}",
"EKWO_ACT_AS_USER_ID": "${EKWO_ACT_AS_USER_ID}"
},
"args": [
"-y",
"@ekwo-ai/mcp"
],
"command": "npx"
}
}
}Documentation
Ekwo OS
Ekwo OS is free and open source data infrastructure for accounting, finance and tax, under AGPL-3.0: a schema, the rules of each country as data, and tools, for businesses and their accountants. Install it on your own Supabase project and own your accounting data, forever. It is software — not an accounting firm, and not accounting, tax or financial advice (DISCLAIMER.md). Built and maintained by Ekwo. A fully managed edition — your own instance, operated for you, with the AI agents you configure acting on your instructions — is available at ekwo.ai.
An empty Supabase project to a prepared VAT return, for two countries side by
side, with the published ekwo-os 0.5.0. How it was recorded, and a longer
cut: docs/demo/.
Try it on your own books. Already using Claude? Start with Claude goes from a free Supabase project to your own books taken over, questioned and your next VAT return prepared — Claude Desktop or Claude Code, about twenty minutes — or open the same books in the web application, Ekwo Cloud.
Why Ekwo
The long version — financial autonomy for every business, accounting as a commons, a network rather than a vendor — is in MANIFESTO.md.
Accounting software has settled into two shapes, and both take something from you. The SaaS keeps your books on its servers, behind its API and its price list, and leaving means exporting a PDF. The open-source ERP gives you the code but keeps the parts that save time — bank feeds, automatic matching, invoice recognition — for the paid edition and a network of integrators.
Ekwo is built on a different premise: the data belongs to the business, and the work of keeping the books can be done by software that the business also owns. So the whole accounting core is open, the data sits in a Postgres database that you control, and the interface is designed for machines as much as for people. A REST API and an OpenAPI description come free with Supabase, and an MCP server sits on top of them, so an AI agent can book a purchase, match a payment, prepare a VAT return or produce a FEC on your own data — as you, under your own row level security, without the data ever leaving your account.
What we are building, in order:
- This repository — the core. Schema, posting rules, VAT, reports, the FEC, and the country packs. Done, tested, installable today.
npx -y ekwo-os@latest init— point it at your own Supabase project and it applies the schema, seeds the country rules, creates the first administrator and the first company, in one command. Done; seepackages/cli.- The MCP server — so any AI agent can operate the books: read the
ledger, raise an invoice, post it, match a payment, pull the VAT return or
the FEC. Two ways to reach it, and they are the same server:
https://mcp.ekwo.ai/mcp, hosted, which a client that speaks Streamable HTTP and OAuth adds as a URL, andnpx @ekwo-ai/mcpover stdio, for a client that launches its servers itself. Done; seepackages/mcp. The web application is Ekwo Cloud, and it opens an instance you run yourself as well as one we host. - Any country as a versioned pack of data, with one golden test per
country. Every folder of
packs/ships today — the list is under What is in this repository — and more arrive every week. The plan is indocs/international.md. - Format libraries as independent MIT packages, in
packages/formats/, organised by format and never by country: the French FEC, Factur-X, XBRL for the NBB, the Belgian VAT return, Peppol BIS Billing 3.0, four recapitulative statements and three readers of bank statements — camt.053, CODA and CFONB 120 — exist today; MT940 waits for somebody who needs it.
Who it is for: a company that wants to keep its own books with an AI at the keyboard; an accounting firm that runs several companies inside one installation; a developer who needs a real double-entry core with VAT rules as data rather than as code; and anyone who wants to leave a proprietary system with the books intact.
What we sell, so that this stays free: a managed edition at ekwo.ai where the same schema runs on your own Supabase project, and Ekwo operates the application, the AI agents you set up, the bank connections, the Peppol access point and the channels to the administrations. It is infrastructure, not a service that keeps the books: the books and the filings stay the business's and its accountant's. The Community edition stands on its own, with us or without us, for as long as its owner wants it to. That is the test every feature has to pass before it lands here.
What is in this repository
A double-entry accounting core for Postgres. It is the schema, the posting rules and the reports, as migrations you apply to a database you control. There is no server to run: Supabase turns the schema into a REST API with an OpenAPI description, and row level security decides who sees what.
- Double entry, enforced by the database. Amounts are positive, a reversal flips the side, an entry cannot be posted unless it balances, and a locked period refuses writes at the trigger — not in a form validator.
- Documents and entries are two layers, joined by a foreign key. An invoice answers to EN 16931 and Peppol; an entry answers to the chart of accounts and to the FEC. Keeping them apart keeps both honest.
- Country rules are data. A tax points at the ledger accounts it posts to and at the boxes of the VAT return it feeds. Adding a régime is a row, not a release.
- Every label is data, in every language the country pack publishes.
Identifiers and error codes are English and never move; what a person reads
is a row. Belgium ships its chart of accounts, its journals, its VAT codes,
its declaration boxes and its annual accounts in French, Dutch, German and
English, and a company keeping its books in Dutch reads Dutch throughout.
docs/languages.mdis the mechanism. - Every country a pack, out of the box. United Arab Emirates (
ae), Albania (al), Argentina (ar), Austria (at), Australia (au), Bosna i Hercegovina (ba), Belgium (be), Burkina Faso (bf), България (bg), البحرين (bh), Bénin (bj), Bolivia (bo), Canada (ca), République démocratique du Congo (cd), Centrafrique (cf), Congo (cg), Schweiz (ch), Côte d’Ivoire (ci), Chile (cl), Cameroun (cm), 中国 (cn), Colombia (co), Costa Rica (cr), Cyprus (cy), Czechia (cz), Germany (de), Danmark (dk), Dominican Republic (do), Algérie (dz), Ecuador (ec), Estonia (ee), مصر (eg), España (es), Finland (fi), France (fr), Gabon (ga), United Kingdom (gb), საქართველო (ge), Ghana (gh), Guinée (gn), Guinée équatoriale (gq), Ελλάδα (gr), Guatemala (gt), Guinée-Bissau (gw), Hong Kong (hk), Croatia (hr), Magyarország (hu), Indonesia (id), Ireland (ie), Israel (il), India (in), Ísland (is), Italia (it), 日本 (jp), Kenya (ke), Comores (km), 대한민국 (kr), Қазақстан (kz), Lietuva (lt), Luxembourg (lu), Latvija (lv), Maroc (ma), Moldova (md), Crna Gora (me), Северна Македонија (mk), Mali (ml), Malta (mt), México (mx), Malaysia (my), Niger (ne), Nigeria (ng), Nederland (nl), Norge (no), New Zealand (nz), Oman (om), Panamá (pa), Perú (pe), Philippines (ph), Polska (pl), Portugal (pt), Paraguay (py), Romania (ro), Srbija (rs), Rwanda (rw), Saudi Arabia (sa), Sverige (se), Singapore (sg), Slovenia (si), Slovensko (sk), Sénégal (sn), Tchad (td), Togo (tg), ประเทศไทย (th), Tunisie (tn), Türkiye (tr), 臺灣 (tw), Tanzania (tz), Україна (ua), Uganda (ug), United States (us), Uruguay (uy), Việt Nam (vn), Kosova (xk) and South Africa (za) — each with its chart of accounts, its tax codes, its declaration boxes and its annual accounts, and each installed byekwo init. The United Kingdom was the first that is not a Member State of the European Union; the United States, with the sales and use taxes of three states and no value added tax, the first without a VAT; Senegal and Côte d'Ivoire the first on the SYSCOHADA chart the OHADA member States share, written once inpacks/ohada/. The version and the certification of each are indocs/packs.md. - The French FEC. Eighteen columns, the arrêté du 29 juillet 2013, with the reconciliation letter and the sub-ledger code the format requires.
- Modules, one Postgres schema each. Fixed assets, budgets and corporate
income tax ship with this release, in
fixed_assets,budgetsandtax. A module depends on the socle by foreign key, reaches the ledger only through one function, and is enabled per company. The socle ignores its modules. - Tested on real Postgres. The test suite runs the migrations, the seeds, the accounting scenarios, the installer and the MCP server against Postgres compiled to WebAssembly.
Modules
The socle is public. Beside it, a module is a schema of its own with its own
migrations, its own row level security and its own tests.
| Module code | Schema | What it does |
|---|---|---|
assets | fixed_assets | Fixed assets, their depreciation schedule and their disposal. Durations, declining coefficients and the prorata convention are country pack data. |
budgets | budgets | A budget per financial year and the variance against what the ledger holds. No country data, and nothing written to the ledger. |
tax | tax | Corporate income tax estimated from the books, line by line: the adjustments, the losses, the rates and their conditions are country pack data, and what a company declares about itself is its own. An estimate until an owner calls it final. Nothing written to the ledger. |
npx -y ekwo-os@latest module list # what is here, and what the database holds
npx -y ekwo-os@latest module migrate # apply their migrations and country seeds
npx -y ekwo-os@latest module enable assets --company "…" # turn one on for a company
Then add the schema to the project's exposed schemas — Supabase dashboard →
Project Settings → API, or [api] schemas in supabase/config.toml. No
migration can do that: it is a setting of the API and not of the database, and
ekwo module enable prints the line every time.
A module never writes the ledger by hand. It hands its lines to
post_module_entry(), which builds the draft and calls post_entry() — so
sides, rounding, numbering and period locks stay in one place. The entry is
tagged (module_code, ref), unique per company, which is what makes running a
depreciation twice a no-op rather than a duplicate. A test over every file of
modules/** refuses a write to entries or entry_lines.
docs/modules.md is how to write one.
Install on your own Supabase project
Create a project at supabase.com — the free plan is enough to start — and point the installer at it. Node 22 or later is the only thing you need locally: no Supabase CLI, no Docker, no clone.
npx -y ekwo-os@latest init
The version is part of the command, as for the MCP server below: from inside a
clone of this repository, a bare npx ekwo-os finds the workspace package of
the same name and answers ekwo: command not found.
Asking an AI agent to do it with you? Point it at
AGENTS.md: what it needs, the commands, and what it must never
do. Once installed, docs/agents.md is its first session in
five steps, on the MCP server and on the command line alike. Each country also has its own step-by-step page on the site, generated
from its pack, and the whole documentation is served to a model as
https://ekwo.ai/llms.txt.
It asks for the connection string, the country, the chart of accounts and the language where the pack offers a choice, your organisation, the first company and the address of the first administrator, then applies the migrations, seeds the chart of accounts and the VAT codes, creates that administrator in your Supabase Auth and runs the six steps below. Every step checks before it acts, so running it twice creates nothing twice.
Ekwo does not create the project and does not pay for it. Your books are on
your account from the first row, which is the only version of "you own your
data" that survives the maintainer going away. Full flags, environment variables and the
non-interactive form are in packages/cli.
What it does underneath
Six steps, in this order. They are ordinary SQL, and running them by hand is a
supported path — with the Supabase CLI, supabase db push applies the same
migrations and writes the same history table the installer does.
-- 1. Record the installation. Once, ever.
select init_instance('My Organisation', 'BE', 'community');
-- 2. Take the administrator seat. The first user to ask takes it; after
-- that, only an administrator can appoint another.
select claim_instance_admin();
-- 3. Create the company. Only an instance administrator may.
insert into companies (name, country, fiscal_country, currency_code)
values ('My Company', 'BE', 'BE', 'EUR')
returning id;
-- 4. Put yourself on its books. Administering the installation is not the
-- same as being a member of a company.
insert into company_members (company_id, user_id, role)
values ('<company-id>', auth.uid(), 'owner');
-- 5. Chart of accounts, journals, taxes and the company's default accounts.
-- The third argument is the language of the labels; left out, the company's.
select install_country_template('<company-id>', 'BE', 'fr');
-- 6. The first financial year.
insert into fiscal_years (company_id, name, start_date, end_date)
values ('<company-id>', 'FY2026', date '2026-01-01', date '2026-12-31');
Steps 1 and 2 are plain inserts underneath — init_instance() writes the
single instance row and claim_instance_admin() writes one row in
instance_admins. The functions exist so the bootstrap rules live in the
database rather than in whichever client happens to run first.
install_country_template copies the chart of accounts, the journals and the
taxes, and wires the company's default accounts — receivable, payable,
suspense, retained earnings — and its journals. It also records, in
company_packs, which version of which country pack this company copied, so
a later release can say what has moved since.
Those seeds are compiled from packs/: a country is a manifest, a
chart of accounts as CSV and a taxes file, and ekwo pack build turns one
into the SQL above. The format is in docs/packs.md.
The installer does steps 1 and 2 in a particular order for a reason worth
knowing. It holds a database connection, not a session, so auth.uid() is
NULL and row level security is bypassed rather than satisfied: it cannot be
the first user. So it creates that user through the Supabase Auth admin API
first — which also needs the service_role key, the only reason the key is
ever asked for — and writes the rows that user will be recognised by second.
By hand instead, with the Supabase CLI:
git clone https://github.com/Ekwo-ai/ekwo-os.git && cd ekwo-os
supabase link --project-ref <your-project-ref>
supabase db push # applies supabase/migrations in order
Then the reference seeds, every one of them and in this order:
psql "$DATABASE_URL" -f supabase/seed/00_currencies.sql
psql "$DATABASE_URL" -f supabase/seed/00_territories.sql
psql "$DATABASE_URL" -f supabase/seed/05_framework_generic.sql
psql "$DATABASE_URL" -f supabase/seed/100_pack_uy.sql
psql "$DATABASE_URL" -f supabase/seed/101_pack_ec.sql
psql "$DATABASE_URL" -f supabase/seed/102_pack_bo.sql
psql "$DATABASE_URL" -f supabase/seed/103_pack_py.sql
psql "$DATABASE_URL" -f supabase/seed/104_pack_do.sql
psql "$DATABASE_URL" -f supabase/seed/105_pack_cr.sql
psql "$DATABASE_URL" -f supabase/seed/106_pack_gt.sql
psql "$DATABASE_URL" -f supabase/seed/107_pack_pa.sql
psql "$DATABASE_URL" -f supabase/seed/108_pack_in.sql
psql "$DATABASE_URL" -f supabase/seed/109_pack_cn.sql
psql "$DATABASE_URL" -f supabase/seed/10_pack_be.sql
psql "$DATABASE_URL" -f supabase/seed/110_pack_ge.sql
psql "$DATABASE_URL" -f supabase/seed/111_pack_md.sql
psql "$DATABASE_URL" -f supabase/seed/112_pack_rs.sql
psql "$DATABASE_URL" -f supabase/seed/113_pack_ba.sql
psql "$DATABASE_URL" -f supabase/seed/114_pack_al.sql
psql "$DATABASE_URL" -f supabase/seed/115_pack_mk.sql
psql "$DATABASE_URL" -f supabase/seed/116_pack_me.sql
psql "$DATABASE_URL" -f supabase/seed/117_pack_bh.sql
psql "$DATABASE_URL" -f supabase/seed/118_pack_om.sql
psql "$DATABASE_URL" -f supabase/seed/119_pack_gh.sql
psql "$DATABASE_URL" -f supabase/seed/11_pack_fr.sql
psql "$DATABASE_URL" -f supabase/seed/120_pack_tz.sql
psql "$DATABASE_URL" -f supabase/seed/121_pack_ug.sql
psql "$DATABASE_URL" -f supabase/seed/122_pack_rw.sql
psql "$DATABASE_URL" -f supabase/seed/123_pack_xk.sql
psql "$DATABASE_URL" -f supabase/seed/124_pack_kz.sql
psql "$DATABASE_URL" -f supabase/seed/12_pack_lu.sql
psql "$DATABASE_URL" -f supabase/seed/13_pack_ee.sql
psql "$DATABASE_URL" -f supabase/seed/14_pack_gb.sql
psql "$DATABASE_URL" -f supabase/seed/15_pack_us.sql
psql "$DATABASE_URL" -f supabase/seed/16_pack_ie.sql
psql "$DATABASE_URL" -f supabase/seed/17_pack_nl.sql
psql "$DATABASE_URL" -f supabase/seed/18_pack_de.sql
psql "$DATABASE_URL" -f supabase/seed/19_pack_es.sql
psql "$DATABASE_URL" -f supabase/seed/20_pack_sn.sql
psql "$DATABASE_URL" -f supabase/seed/21_pack_ci.sql
psql "$DATABASE_URL" -f supabase/seed/22_pack_bj.sql
psql "$DATABASE_URL" -f supabase/seed/23_pack_bf.sql
psql "$DATABASE_URL" -f supabase/seed/24_pack_cm.sql
psql "$DATABASE_URL" -f supabase/seed/25_pack_cf.sql
psql "$DATABASE_URL" -f supabase/seed/26_pack_km.sql
psql "$DATABASE_URL" -f supabase/seed/27_pack_cg.sql
psql "$DATABASE_URL" -f supabase/seed/28_pack_ga.sql
psql "$DATABASE_URL" -f supabase/seed/29_pack_gn.sql
psql "$DATABASE_URL" -f supabase/seed/30_pack_gw.sql
psql "$DATABASE_URL" -f supabase/seed/31_pack_gq.sql
psql "$DATABASE_URL" -f supabase/seed/32_pack_ml.sql
psql "$DATABASE_URL" -f supabase/seed/33_pack_ne.sql
psql "$DATABASE_URL" -f supabase/seed/34_pack_cd.sql
psql "$DATABASE_URL" -f supabase/seed/35_pack_td.sql
psql "$DATABASE_URL" -f supabase/seed/36_pack_tg.sql
psql "$DATABASE_URL" -f supabase/seed/37_pack_it.sql
psql "$DATABASE_URL" -f supabase/seed/38_pack_id.sql
psql "$DATABASE_URL" -f supabase/seed/39_pack_my.sql
psql "$DATABASE_URL" -f supabase/seed/40_pack_au.sql
psql "$DATABASE_URL" -f supabase/seed/41_pack_nz.sql
psql "$DATABASE_URL" -f supabase/seed/42_pack_mx.sql
psql "$DATABASE_URL" -f supabase/seed/43_pack_pt.sql
psql "$DATABASE_URL" -f supabase/seed/44_pack_ph.sql
psql "$DATABASE_URL" -f supabase/seed/45_pack_ar.sql
psql "$DATABASE_URL" -f supabase/seed/46_pack_cl.sql
psql "$DA
Sourced from the repository README.
More in AI & Agents
- PonytailMakes your AI agent think like the laziest senior dev in the room. The best code is the code you never wrote.109,599
- AgentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity39,079
- Frontend SlidesCreate beautiful slides on the web using a coding agent's frontend skills28,060
- Agent Skills Search ServerSearch and discover Agent Skills from the skills.sh registry. Powered by HAPI MCP server.25,980
- Agency Agents Zh🎭 267 个即插即用的 AI 专家角色 — 支持 Hermes Agent/Claude Code/Cursor/Copilot 等 18 种工具,覆盖工程/设计/营销/金融等 20 个部门。含 52 个中国市场原创智能体(小红书/抖音/微信/飞书/钉钉等)。搭配编排器 agency-orchestrator,一句话即可让多位专家按 DAG 自动协作。19,868
- Watermarks RemoverStrip multi-vendor AI provenance marks: Unicode text hygiene, statistical rewrite hooks, and C2PA/metadata from PNG/JPEG/SVG/PDF/DOCX/HTML/MD17,822