An accounting firm, a fiduciary, a bookkeeper with thirty customers, a group that keeps the books of its three subsidiaries: somebody who keeps several companies that are not all theirs. This page is what Ekwo gives them, where each piece lives, and what is still missing.
It is the normal case of this schema and not an edition of it. There is no
"firm plan", no tenant_id, and no table called firms.
The model in four sentences
- One installation belongs to one customer — here, the firm.
instanceis that fact, in one row. - Every client of the firm is a
companiesrow inside it, with its own chart, its own journals, its own locks and its own country pack. Forty clients in six countries is forty rows and six packs. - A person has a role per company, in
company_members. A collaborator of the firm isaccountanton the companies they keep; the person who runs a client company isclienton that one, and does not know the others exist — row level security answers them as if the installation held one company. - The firm is therefore nothing but the people who hold rights on several companies. Which is why the word needs a definition below.
What "portfolio" means here
Portfolio — the set of companies a caller may read, worked out at the moment of the call. For a collaborator of an accounting firm it is the clients they keep: the firm's client portfolio, in the sense the profession uses the word (portefeuille de clients, klantenportefeuille, Mandantenstamm). It has nothing to do with investments.
The word appears in two functions, and means the same thing in both:
| Function | The question a firm asks on Monday morning |
|---|---|
portfolio_upcoming_filings(from, to) |
Across all my clients, which returns fall due between these two dates — and which have I not started? |
portfolio_filings_touched_since(from, to) |
Across all my clients, which returns that already went have had their period written to since? |
Each is the per-company reading (upcoming_filings(),
filings_touched_since()) asked of every company of the portfolio, with the
company named on each row. See filing.md
for the columns.
Nobody maintains a portfolio. It is not a list, a table or a setting: it is
"the companies on which the caller holds filings.read", computed each time.
So it follows the rights, and cannot drift from them:
| Who calls | Their portfolio |
|---|---|
A collaborator who is accountant on forty companies |
those forty |
| A collaborator who joined last week and was given two | those two |
The person who runs a client company (client) |
that one company — the same function serves them |
Somebody whose filings.read was revoked on a company |
every company but that one |
| The administrator of the instance, member of no company | none: creating companies is not keeping their books |
The functions run as the caller (security invoker) and name the capability
they filter on, because a company's row is visible to people its
declarations are closed to.
Every company of the portfolio is in every answer, at least once. A company
with nothing due is a row that says nothing_due; one whose pack names no
deadline is listed without a date and says no_deadline_rule. A client missing
from the list a firm plans its fortnight on is worse than a client listed
without a date.
The client is a guest with narrow rights
The client preset is the person who runs a company whose books the firm
keeps. They read everything that is theirs — documents, the ledger, the
frozen declarations and the proofs of deposit — they hand pieces over
(documents.deposit: an attachment on their own company, signed by them,
insert only), and they write nothing else. Inviting one is one call:
select * from invite_member(:company, 'owner@client.example', 'client');
The ledger is included on purpose: the books are the client's, and the firm is the guest who keeps them. A firm that wants to show less revokes a capability on that member; see Who may do what in the README.
A client is never counted, licensed or charged for in the open core.
The firm files in its own name, for all of them at once
Where the format of a country allows it, a representative deposits the returns
of all its clients in one file. The Belgian periodic VAT return does: the brick
@ekwo-ai/vat-consignment
writes one VATConsignment holding as many declarations as it is given, under
the Representative who files them — each taken from the figures that company
froze, never recomputed.
Sending the file is the operated side (ee/). What comes back — the deposit
number, the acknowledgement, the administration's words — is recorded in each
client's own tax_filing_deposits, in the open core: a client who leaves keeps
every proof that their returns were filed.
Two ways to arrange a firm and a client
| A. The firm's installation | B. The client's installation | |
|---|---|---|
| Who owns the installation | the firm | the client |
| The client is | a guest (client) |
the owner; the firm's collaborator is invited as accountant |
| Suits | small businesses and the self-employed: the firm does everything | a company that keeps part of its own books |
| The portfolio | one call, in the firm's installation | crosses installations, so it is a control plane: ee/ |
| The client leaves | export_company(): their company, whole, as an archive another installation takes in |
pg_dump, and nothing else |
Both are meant to exist. A is what this schema serves today.
The client leaves with the books
Arrangement A is only honest if the way out exists, and it does. A company
leaves an installation as an archive — entries, documents, pieces, the bank,
the declarations with the figures they were frozen with and the proof each one
went, the trail — and arrives in another installation as a living company, with
its numbers, its locks and its identifiers. ekwo company export and
ekwo company import from a shell; export_company() and import_company()
underneath. The format and every check are in
company-archive.md.
The client can do it themselves. Leaving needs company.export, and the
client preset holds it as the owner preset does: a right to leave that only
the firm could exercise would be a courtesy of the firm. The firm's
collaborators (accountant) do not hold it. A firm with a reason to withhold
it revokes it for that member, and the revocation is on the audit trail of the
company.
What stays in the firm's installation: who was a member, the invitations, the machine keys, the shared links — and the company itself, since nothing deletes one yet. What the archive does not carry: the files the attachments point at, which the manifest lists for somebody to copy.
What is missing, in the order it matters
- Carrying the files of the attachments, and removing a company once it
has left. A company can now be extracted and imported elsewhere
(
company-archive.md); the bytes of its pieces are listed and not moved, and the original stays where it was. - The mandate as an object: who may file for whom, with which
administration, from when to when.
tax_filing_deposits.sent_bysays who sent; nothing says in what capacity. The brick that writes the representative does not check that one exists. - Groups of collaborators. Giving somebody forty companies is forty rows of
company_memberstoday. - Separation of duties between whoever prepares a return and whoever files
it: the
readystate exists for it, the rule that forbids one person both gestures does not. An API key has no portfolio.Delivered: a key is on the company it was minted on, so its portfolio is that company (machine-access.md). What is still missing is a key across a firm's whole portfolio, which is one key on several companies and therefore a different object.- The portfolio across installations (arrangement B), which is
ee/.
What is deliberately not here: billing a firm's clients and tracking its time
(the firm's trade, not its accounting), and a tenant_id that would let several
firms share a database.