Disclaimer: This repository is not affiliated with, endorsed by, or connected in any way to Xero Limited or the official Xero product. Naming and UI patterns are inspired only by Xeroβs public documentation and familiar workflows; all trademarks belong to their respective owners.
Over the last few years, prices for comparable cloud accounting software have gone up several times, while the slice of functionality I actually use has not kept pace. I was tired of paying for unused surface area, so I started goXero as a free, open-source alternative you can self-host and adapt to your own needs.
A Xero.com-style accounting platform β open-source clone built on top of the Xero Accounting API data model.
OrganisationID, resolved from
the Xero-Tenant-Id HTTP header (same convention as Xero) and validated
against the authenticated user's organisation_users membership β a valid
JWT alone is not enough to access a tenant.goxero/
βββ cmd/
β βββ server/ # Fiber v3 HTTP server entrypoint
βββ scripts/
β βββ migrate # goose migrations (up/down/status/create; needs goose CLI)
β βββ _helper # shared bash helpers for scripts
βββ internal/
β βββ config/ # Env-driven configuration
β βββ database/ # pgx pool bootstrap
β βββ handlers/ # Fiber handlers (auth, invoices, contacts, β¦)
β βββ middleware/ # JWT auth + tenant resolver
β βββ models/ # Domain structs (mirrors Xero response shapes)
β βββ repository/ # pgx data-access layer
β βββ router/ # Route registration & middleware wiring
β βββ logger/ # slog helpers
βββ migrations/ # goose SQL migrations (+ fixtures/ for dev-only data)
βββ web/ # SvelteKit frontend (Tailwind)
β βββ src/
β β βββ lib/ # API client, stores, types, utils
β β βββ routes/ # /login, /register, /app/*
β βββ β¦
βββ compose.dev.yml # Postgres dev & test services
βββ AGENTS.md # Agent-facing map of the repo
βββ Makefile # Convenience commands
βββ .env.example
make docker-up
cp .env.example .env
go install github.com/pressly/goose/v3/cmd/goose@latest # once: standalone goose CLI
make migrate # ./scripts/migrate up β schema + demo seed only
Optional second tenant + draft invoice (real UUIDs) for local dev / integration tests:
./scripts/migrate dev up # core up, then migrations/fixtures (see migrations/fixtures/README.md)
Seed creates:
| What | Value |
|---|---|
| Organisation | Demo Company (Global) β 6823b27b-c48f-4099-bb27-4202a4f496a2 |
| Admin email | [email protected] |
| Admin password | admin123 |
| Accounts | A simplified default Chart of Accounts |
| Tax rates | Tax-exempt / Sales / Purchases |
| Contacts | 3 demo customers & suppliers |
| Items | CONS-01, WIDGET |
make run # listens on :8080
Requires Bun on PATH.
make web-install # bun install in web/
make web-dev # http://localhost:5173 with proxy to :8080
Open http://localhost:5173 and sign in with
[email protected] / admin123.
goXero exposes two groups of endpoints:
| Method | Path | Purpose |
|---|---|---|
| POST | /api/auth/register |
Create a user + issue JWT |
| POST | /api/auth/login |
Email/password login β JWT + tenants |
| GET | /api/auth/me |
Current user + accessible organisations |
| GET | /api/organisations |
All organisations the user can access |
| GET | /health |
Liveness probe |
All under /api/v1/β¦, authenticated with Authorization: Bearer <JWT> and
Xero-Tenant-Id: <OrganisationID> (exactly as Xero does).
| Resource | Endpoints |
|---|---|
| Organisation | GET /organisation |
| Accounts | GET/POST /accounts Β· GET/PUT/DELETE /accounts/:id |
| Tax rates | GET/POST /tax-rates Β· GET/PUT/DELETE /tax-rates/:id |
| Contacts | GET/POST /contacts Β· GET/PUT /contacts/:id |
| Contact groups | GET/POST /contact-groups Β· GET/PUT/DELETE /contact-groups/:id Β· PUT /contact-groups/:id/contacts Β· DELETE /contact-groups/:id/contacts/:contactId |
| Items | GET/POST /items Β· GET/PUT /items/:id |
| Invoices | GET/POST /invoices Β· GET/PUT/DELETE /invoices/:id Β· GET /invoices/:id/payments |
| Credit notes | GET/POST /credit-notes Β· GET/PUT/DELETE /credit-notes/:id Β· PUT /credit-notes/:id/allocations |
| Payments | GET/POST /payments Β· GET /payments/:id Β· DELETE /payments/:id (void) |
| Prepayments | GET/POST /prepayments Β· GET/PUT /prepayments/:id |
| Overpayments | GET/POST /overpayments Β· GET/PUT /overpayments/:id |
| Bank transactions | GET/POST /bank-transactions Β· GET/DELETE /bank-transactions/:id |
| Bank transfers | GET/POST /bank-transfers Β· GET /bank-transfers/:id |
| Manual journals | GET/POST /manual-journals Β· GET/DELETE /manual-journals/:id |
| Journals (GL feed) | GET /journals |
| Quotes | GET/POST /quotes Β· GET/PUT/DELETE /quotes/:id |
| Purchase orders | GET/POST /purchase-orders Β· GET/PUT/DELETE /purchase-orders/:id |
| Repeating invoices | GET/POST /repeating-invoices Β· GET/PUT/DELETE /repeating-invoices/:id |
| Batch payments | GET/POST /batch-payments Β· GET /batch-payments/:id |
| Linked transactions | GET/POST /linked-transactions Β· GET/PUT/DELETE /linked-transactions/:id |
| Employees | GET/POST /employees Β· GET/PUT/DELETE /employees/:id |
| Receipts | GET/POST /receipts Β· GET/PUT /receipts/:id |
| Expense claims | GET/POST /expense-claims Β· GET/PUT /expense-claims/:id |
| Currencies | GET/POST /currencies |
| Branding themes | GET/POST /branding-themes |
| Tracking | GET/POST /tracking-categories Β· GET/PUT/DELETE /tracking-categories/:id Β· PUT /tracking-categories/:id/options |
| Users | GET /users |
| Bank feeds | GET /bank-feeds/providers Β· GET /bank-feeds/institutions Β· GET/POST /bank-feeds/connections Β· GET/DELETE /bank-feeds/connections/:id Β· POST /bank-feeds/connections/:id/finalize Β· POST /bank-feeds/connections/:id/sync Β· PUT /bank-feeds/accounts/:feedAccountId Β· GET /bank-feeds/statement-lines Β· POST /bank-feeds/statement-lines/:id/import Β· POST /bank-feeds/statement-lines/:id/ignore |
| Attachments | GET /:subject/:id/attachments Β· POST /:subject/:id/attachments/:fileName Β· GET /:subject/:id/attachments/:attachmentId |
| History | GET /:subject/:id/history Β· PUT /:subject/:id/history |
| Reports | GET /reports/trial-balance Β· /profit-and-loss Β· /balance-sheet Β· /aged-receivables Β· /aged-payables Β· /bank-summary Β· /cash-summary Β· /executive-summary Β· /budget-summary Β· /bas Β· /journal-report Β· /invoice-summary |
:subject accepts invoices, credit-notes, bank-transactions, contacts,
accounts, manual-journals, quotes, purchase-orders, receipts,
expense-claims.
goXero ships with an adapter for GoCardless Bank Account Data (ex-Nordigen, free PSD2 tier covering 2 500+ EU/UK banks). Hooking it up:
Create an account at bankaccountdata.gocardless.com
and generate secret_id / secret_key under User Secrets.
Drop them into .env:
GOCARDLESS_BAD_SECRET_ID=...
GOCARDLESS_BAD_SECRET_KEY=...
BANKFEED_REDIRECT_URL=http://localhost:5173/app/bank-feeds/callback
Restart the server β goXero discovers the adapter and exposes
/api/v1/bank-feeds/* for the authenticated tenant.
Consent flow:
# 1) pick a bank
curl "$API/bank-feeds/institutions?provider=gocardless_bad&country=GB"
# 2) create a connection (returns AuthURL β redirect the user there)
curl -X POST $API/bank-feeds/connections \
-H "Content-Type: application/json" \
-d '{"Provider":"gocardless_bad","InstitutionID":"SANDBOXFINANCE_SFIN0000"}'
# 3) after the user returns, finalise to discover accounts
curl -X POST $API/bank-feeds/connections/<id>/finalize
# 4) pull transactions (idempotent β re-run as often as you like)
curl -X POST $API/bank-feeds/connections/<id>/sync
# 5) review the staging inbox, then import the rows you want posted
curl "$API/bank-feeds/statement-lines?status=NEW"
curl -X POST $API/bank-feeds/statement-lines/<lineId>/import
Statement lines are deduped by (FeedAccountID, ProviderTxID), so re-syncing
never double-counts. Rows you don't want posted can be hidden via
/bank-feeds/statement-lines/<lineId>/ignore. Adding another aggregator
(Plaid, TrueLayer, Salt Edge) is purely a new Provider implementation under
internal/bankfeed/ β no schema or handler changes.
GET list responses follow Xero's envelope ({ Id, Status, ProviderName, DateTimeUTC, Payload: { <Resource>: [...], Pagination? } }). POST/PUT return
the raw { <Resource>: [...] } payload to match Xero.
Reports are computed on the fly from General Ledger journal lines
(gl_journal_lines table, populated centrally by internal/repository/gl.go)
β every posted invoice, credit note, bank transaction and manual journal writes
balanced entries there. When the dedicated tax control account (code 820) is
missing from a tenant's chart of accounts, the tax portion is folded into the
revenue/expense line so the journal still balances to zero.
curl "http://localhost:8080/api/v1/invoices?type=ACCREC&status=AUTHORISED" \
-H "Authorization: Bearer $TOKEN" \
-H "Xero-Tenant-Id: 6823b27b-c48f-4099-bb27-4202a4f496a2"
curl -X POST http://localhost:8080/api/v1/invoices \
-H "Authorization: Bearer $TOKEN" \
-H "Xero-Tenant-Id: 6823b27b-c48f-4099-bb27-4202a4f496a2" \
-H "Content-Type: application/json" \
-d '{
"Type": "ACCREC",
"ContactID": "<contact-uuid>",
"Date": "2026-04-18",
"DueDate": "2026-05-18",
"LineAmountTypes": "Exclusive",
"Status": "DRAFT",
"LineItems": [
{ "Description": "Consulting", "Quantity": 10, "UnitAmount": 150, "AccountCode": "200" }
]
}'
The SvelteKit app mirrors Xero's familiar information architecture:
ACCREC / ACCPAY), filters, search, pagination, detailed view with approve / void / record payment actions.reports/invoice-summary.Styling: custom teal-first brand palette (brand-*) plus Inter font.
Components use Tailwind utilities with a small set of semantic classes
(.btn-primary, .card, .badge-paid, β¦) in src/app.css.
make help # full list
make docker-up # start Postgres
make migrate # ./scripts/migrate up (core only)
./scripts/migrate dev up # core + SQL fixtures (extra org & invoice)
make migrate-down # rollback last
make migrate-reset # roll back all migrations
make migrate-status # list status
make migrate-create NAME=x # new migration (goose create)
make run # start Fiber server
make build # compile binaries to ./bin
make test # go test ./... (needs pgtestdb on :5433 unless PGTESTDB_SKIP=1)
make vet # go vet ./...
make web-install # bun install in web/
make web-dev # vite dev server
make web-build # production bundle
make web-check # svelte-check (types + a11y)
config.Load() fails fast when SERVER_PORT, DB_PORT, JWT_ACCESS_TTL,
etc. are malformed β no more silent fallbacks.APP_ENV=production and the stock JWT_SECRET aborts the
boot so insecure defaults never reach a production deployment.The task asked for "a clone of xero.com based on the Accounting API docs" which is very large. Sensible defaults were chosen and documented below:
Xero-Tenant-Id header is honoured so real
Xero SDKs can point at goXero with minimal friction.NUMERIC(18,4) and the Go
shopspring/decimal type (same precision Xero uses).Exclusive pricing the backend sums
qty Γ price β discount, tax comes from the payload. Inclusive / NoTax
paths preserve the supplied totals (mirrors Xero's behaviour).00009_seed_demo.sql populates a demo
organisation, admin user, chart of accounts, tax rates, contacts and items
so the UI has something to display right after make migrate.web/, Bun for
install / run; Vite dev proxy for /api so both sides run
concurrently without CORS hassle.This project is free / open-source software, provided βas isβ and without warranties of any kind, whether express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, title, and non-infringement.
No liability: To the fullest extent permitted by applicable law, the maintainers, contributors, and copyright holders disclaim all liability for any direct, indirect, incidental, special, exemplary, or consequential damages, or any loss of profits, data, goodwill, or business interruption, arising out of or in connection with the use or inability to use this software β even if advised of the possibility of such damages.
Not professional advice: goXero is not certified or audited accounting, tax, payroll, or legal software. Nothing in this repository constitutes financial, tax, legal, or regulatory advice. You alone are responsible for the accuracy of your books, filings, integrations, backups, security, and compliance with the laws and professional standards that apply to you and your organisation.
Use at your own risk: Deploying goXero for production or regulated workloads is entirely your decision and your responsibility, including obtaining any licences, insurance, or professional review you may require.
PRs welcome :)