Your cost base is not ours to read.
Every installer who uses Jouli is a competitor of the installer who built it. So the question is not whether we promise to look away. It is whether the software can reach your margin at all.
This page describes the system as it is built today, 6 October 2026. Where a protection is a design property rather than an audited control, it says so. Jouli holds no SOC 2 report and no ISO certification, and has not been independently penetration tested. We would rather you learn that here than in a procurement questionnaire.
Five things a buyer actually asks.
In the order they get asked, with the honest answer rather than the comfortable one.
org_id, and the
data layer refuses a query that does not name one.Cloudflare, and nothing else of consequence.
One worker, one database, one object store. The shorter that list is, the fewer places a mistake can hide.
Cloudflare D1
The operational database: proposals, leads, the roster, sessions, the email ledger and the audit log. Reached only from the Jouli worker — it is not exposed to the internet and there is no public connection string.
Cloudflare R2
Object storage, used for one thing that matters here: the snapshot of a signed document. Keyed by job ID and document version. Written once, read back by the tenant's own team, never rewritten.
Nothing cached in the middle
Every authenticated response carries Cache-Control: private,
no-store. The customer-facing contract is served from the installer's own
domain, not from ours, because it carries their ABN and bank details, not Jouli's.
Data residency is not pinned
The business is run from Australia, but we have not configured a contractual residency guarantee for the database's primary region, so we will not claim one. If residency is a requirement in your procurement, ask before you sign and we will confirm the configured region in writing.
What we do not run. No analytics vendor inside the cockpit, no session replay, no third-party tag manager, no data warehouse copy. Outbound mail goes through one provider and Xero receives deposit invoices — both are named in the privacy policy rather than left to be discovered.
A forgotten WHERE clause is how this goes wrong.
Not a hack. A handler written next year that queries a table and forgets one condition. It fails silently: the query succeeds and returns somebody else's rows. So the rule is enforced by the thing that prepares the statement, not by a reviewer reading a diff.
A default-deny data layer
Handlers do not reach the database directly. They take a handle scoped to the
company on the current session. That handle refuses two things outright: a request
with no company attached, and any statement that touches a tenant table without
naming org_id. Both raise an error. Neither returns rows.
The failure mode is a 500 and a log line, not a leak. That is the trade made on purpose.
A test that walks every statement, not a checklist
Per-endpoint security tests go stale the moment someone adds an endpoint. So a test
suite parses the worker source and enumerates every database statement in it. Each one
that touches a tenant table must either carry org_id or appear
in a written exception list with a reason. It fails in both directions: a new unscoped
statement fails because it is not on the list, and a statement that gets scoped fails
because its exception is now stale.
It runs on every push. The number below is the remaining work, and it can only shrink.
What the six are, plainly
All six are writes or counts, not reads that return another company's proposals: the bootstrap of a company's very first owner account, the write to the email ledger, the public website lead capture and its delivery flag, and the mail bounce webhook. Each one is blocked on the same decision — how a company is provisioned, and where per-company credentials live — and each is listed in the test with what would unblock it.
They have to be closed before a second company is on the platform. That is a statement of sequence, not a disclaimer.
No special case for client number one
Mission Green is an ordinary tenant whose settings row happens to have everything enabled. There is no branch in the code that names it. The test applied to every change is not "does Mission Green still work", it is "would a second company get the same behaviour with no new code". A platform that special-cases its first customer cannot onboard its second.
The operator cannot see your numbers. That is structural, not a policy.
Your cost base, labour rates, target margin, discount cap, hard floor and concession pool are the most commercially sensitive numbers you have. They are also exactly what proposal software has to hold to be useful. Here is how those two facts are reconciled.
The platform plane gets no handle
Jouli's own operating plane is company zero. The data layer refuses company zero the same way it refuses a missing company: it throws rather than returning a database handle. The platform cannot read a tenant's margin by accident because it cannot open the connection in the first place.
A credential with no company reads nothing
There is one administrative credential that is minted from a global token and has no roster row, so no company honestly owns it. Every route that would serve company data refuses it with a clear error and tells the holder to sign in as themselves.
Two memberships, never one session
The same person owns Mission Green and operates Jouli. Those are two separate memberships, and a session carries one of them. If a single sign-in ever carried both authorities, every guarantee on this page would be theatre.
Support impersonation
There is no "view as this customer" function, because the only safe version of it is time-boxed, approved in-app by the tenant's own owner, logged where the tenant can read the log, and still unable to unmask money columns. Until it is built that way, it is not built. Support asks you to look at your own screen.
Say the quiet part. Jouli's first customer is the installer that built it. If a Jouli operator could read a competitor's cost base, the product would be asking installers to hand their margin to a rival. "We promise we don't look" is not an answer to that, so the answer is in the data layer instead.
There is no password to steal.
Nobody sets one, so nobody reuses one, and there is no password database to breach.
No SSO, no SAML and no two-factor step beyond control of the mailbox today. If your IT requires enterprise SSO, we do not have it and will tell you so on the call rather than after the contract.
Five roles, and nobody sees a number it is not their job to set.
One record, five levels of access inside a company. Access is granted by that company's own owner, from inside the cockpit, and every grant and withdrawal lands in the audit log.
Scoping a rep to their own jobs is one database condition, and a precedence mistake in it would return every company's matching rows. It is tested, and the test is the kind that fails loudly rather than quietly passing.
A contract that exists in one row is not a record.
A signature is the one event in this system that can never be reconstructed later. So it is written twice.
The customer signs in the browser
The signature, the time, the version signed and the price at that moment are written to the job.
A snapshot is written to object storage, once
The document the customer actually saw, the configuration it was priced from, the signature record and the identifier of the pricing engine that produced it — stored under the job and version. A later migration, purge or mistaken delete on the database cannot take it with them.
It is never overwritten
A new version gets a new key. The accepted version is the one served to the customer from then on, so the document behind a signature cannot quietly change.
The company's own team can read it back
On an authenticated route, scoped to that company. Snapshots begin on 11 September 2026; for anything signed before that date the database row is the record, and the endpoint says so rather than returning nothing.
Numbers, so this is checkable. 65 proposals built and sent, 10 accepted with a snapshot kept, 161 emails logged against jobs. One client — the installer that built the software — running its entire quoting operation on it for months.
A retention promise nobody deletes against is a sentence in a policy.
An unsigned proposal carries a name, an address, a phone number, an electricity bill and a meter identifier. Two years on it has no record-keeping reason to exist.
730 days, and it is your setting
Retention is a value on your company's row, not a platform constant. 730 days by default. Set it shorter and the purge honours the shorter number from that day on.
A purge that actually runs
A scheduled job runs once a day, reads each active company's retention setting, and deletes that company's unsigned proposals past the cutoff — drafts, sent-and-never- accepted, and voided. It is idempotent, and the time of the last run is reported on the service's own health endpoint, so "it runs" is a thing you can check rather than a thing we assert.
Signed contracts are kept
They are the record. A signed job and its snapshot are exempt from the purge and are kept for as long as you are a customer. Deleting the evidence of an agreement is not a privacy feature.
Each pass touches one company
The scheduled job has no session, so it takes a handle for exactly one company at a time and binds the delete to it. A cron is the easiest place in a system to write an unscoped delete, which is why it goes through the same default-deny layer as everything else.
Your data is yours, including on the way out.
The commitment in the commercial terms, and the state of the code behind it. They are not the same thing yet, and pretending otherwise is the sort of claim this page exists to avoid.
Export
Today: the finance view and the signed-contract ledger both download as CSV from inside the cockpit, scoped to your company.
Committed, not yet built: one-click export of every proposal, lead and ledger row as JSON and CSV. It is in the commercial terms as an obligation. It is not a button yet, and we will not draw one on a screenshot until it is.
On exit
Your company's row is deactivated, which stops sending and stops new work. Your retention setting keeps applying, so the purge removes unsigned work on the schedule you chose rather than on ours.
No lock-in by data hostage. If the export you need does not exist yet, ask and we will produce it by hand, from your company's rows, before you go.
Per-company credentials
The connections to accounting, mail, mapping and the CRM are single-tenant today: one set of credentials, held by the platform, for the one company on it. Splitting those per company is the open decision that several items on this page are waiting behind, and it has to be resolved before a second company goes live.
The list that usually gets left off.
Everything here is either absent or unfinished. If a vendor's security page has no such list, that is information about the page, not about the software.
If you are doing diligence on this, the questions worth asking us are:
- Show me the test that walks every database statement, and the six exceptions.
- Open a second company and prove the first cannot see it.
- Put your own cost base in, then ask a Jouli operator to read it back to you.
- Set retention to 30 days and come back in a month.
All four are demonstrable today. The last one takes a month, which is the point of it.
Ask the hard question first.
We would rather lose a deal on this page than win one and have it come up later.