Source-agnostic data model

Why a customer from SQL Account, Xero, or manual entry all look identical in Kiravy.

The most important design decision in Kiravy is that where data came from doesn't change what it looks like. A customer synced from SQL Account, a customer synced from Xero, a customer you typed in manually — they all end up in the same customers table with the same shape. The only difference is a source column that says where it originated.

This page explains why we did it that way and what that means for you.

The principle in one sentence

A customer is a customer is a customer. Same for suppliers, invoices, transactions, chart of accounts.

The accounting software the SME uses is an implementation detail of how the data gets in, not a property of the data itself. So Kiravy stores it in a uniform shape regardless of source.

What that looks like in practice

Every "domain" table in Kiravy — customers, suppliers, ar_invoices, ap_invoices, transactions, coa_accounts — has the same trio of columns:

ColumnPurpose
sourceWhere this row came from (integration / manual / receipt / bank / csv)
external_idThe upstream system's stable identifier (e.g. arcust_CUST001 for an SQL Account customer)
(the actual fields)Name, code, amounts, dates — everything the SME would recognise

The (source, external_id) pair is what lets future sync runs upsert — find the existing row, update it, no duplicates.

Why this matters

1. The UI doesn't care where data came from

When you open Client → Customers, you see all the customers regardless of where they were imported from. No "SQL Account tab" + "Xero tab" + "manual tab" — just one list.

The same is true for transactions, invoices, suppliers, the chart of accounts. One screen, all sources, distinguished only by a small source badge if you care to look.

2. Migration between accounting software is painless

A client switching from SQL Account to Xero would normally be a nightmare — three months of overlap, two systems of record, reconciliation hell.

In Kiravy: you stop the SQL Account agent, link Xero, and the new sync starts populating the same tables. Old data has source = 'integration' + external_id starting with arcust_…; new data has source = 'integration' + external_id starting with xero_…. They coexist in one customer list. You see the cutover in the row dates, not in two separate UIs.

(Today only SQL Account is wired up — Xero and QuickBooks are scaffolded but not live. The design accommodates them when they ship.)

3. Manual entry doesn't feel second-class

If you add a customer by hand (planned UI; today only via integration), it goes into the same table with source = 'manual', external_id = null. It shows up alongside integration-sourced customers in the same list, filterable, searchable, just like any other row.

4. Reports and AI work the same regardless of source

The P&L doesn't care if your office rent came from SQL Account or a receipt OCR — both are transactions rows with account_type = 'expense'. Kiravy AI can answer "what did this client spend on rent?" without knowing or caring how the data got in.

The two boundaries that matter

The model isn't completely source-blind — there are exactly two places where source matters:

Boundary 1: The integration banner

Transactions with source = 'integration' show a small banner on the detail page:

This transaction was imported from your accounting software. It records where the number originally came from.

Reason: these rows came in through a one-time migration from SQL Account. The banner tells you the row's origin so you know it wasn't typed into Kiravy by hand. Once the data is in Kiravy, Kiravy's ledger is the system of record — there's no live connection back to SQL Account, so nothing you do here changes the old system, and the old system won't overwrite your work.

Boundary 2: De-dup via external_id

When a sync runs again, Kiravy needs to know "is this row new, or already here?". That's done via the (workspace_id, source, external_id) unique index:

  • If the external_id already exists for this source → update
  • If it doesn't → insert

That's why we don't accidentally double-import the same SQL Account customer every night. It's also why manual rows leave external_id blank — they don't have an upstream id to match against.

What "source-agnostic" doesn't mean

Two things people sometimes assume that aren't true:

It doesn't mean "Kiravy invents a uniform code system"

Source agnostic at the storage shape, not at the identifier. Your SQL Account customer keeps CUST001 as its code. Your future Xero customer would keep its Xero contact number. They live in the same table, but their code fields aren't reconciled. See Chart of Accounts → How Kiravy thinks — the same principle applies to customers and suppliers, with the same rationale.

It doesn't mean "you can sync from two integrations into the same client"

In practice, one client uses one accounting software at a time. The data model would tolerate two simultaneous integrations (different external_id prefixes wouldn't collide), but the UX would suffer — two rent expenses with different codes, reconciliation ambiguity, AI confusion. The product enforces one integration per client at a time by convention.

When a client genuinely migrates from one software to another, the old integration data stays in the table; the new integration starts adding alongside it. Reports across the cutover date show the mix, sorted by date.

The implications for a firm running many clients

If your firm has:

  • 20 clients on SQL Account
  • 5 on Xero (when that ships)
  • 2 on QuickBooks (ditto)
  • 3 with no accounting software (just receipts + manual)

…you see them all in one Kiravy. Same UI per client, same workflows, same reports. The choice of upstream software is something the SME owner sees, not something the firm has to learn three different products to manage.

That's the whole point.

How this connects to other Kiravy decisions

  • Chart of Accounts codes pass through unchanged — same source-of-truth principle, see How Kiravy thinks about CoA
  • account_type is the only thing Kiravy normalises — coarse classification (revenue / cogs / expense / asset / liability / equity / tax), not codes
  • Cancellation flags from integrations are respected — Kiravy skips cancelled rows rather than carrying them forward as ghosts
  • Status fields (paid / overdue) are computed by trigger from the data — so AR/AP statuses are consistent whether the rows arrived from integration or were manually entered

What's coming

  • Xero / QuickBooks live integrations — first real test of the model with a non-SQL Account source
  • Bank feedssource = 'bank' already in the enum; ingestion path is the next big piece
  • Manual create UI — for clients with no accounting software, type their customers / suppliers / invoices directly

What's next