● Security & data handling

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.

01 — The short version

Five things a buyer actually asks.

In the order they get asked, with the honest answer rather than the comfortable one.

Can another tenant see my jobs? No. Every row on a tenant table carries an org_id, and the data layer refuses a query that does not name one.
Can Jouli see my cost and margin? Not by design, and not by code path. There is no platform account that can hold a tenant database handle. See section 03.
What happens to a signed contract? It is copied to write-once object storage at the moment of signature, keyed by job and version, and never overwritten.
How long do you keep my data? Your choice. 730 days by default for unsigned work, set per company, deleted by a purge that runs every day.
Are you certified? No. No SOC 2, no ISO 27001, no external audit. One client, one codebase, and the controls described here.
02 — Where the data lives

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.

Live

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.

Live

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.

Live

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.

Be careful here

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.

03 — Tenant isolation

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.

Live

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.

Live

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.

88statements against tenant tables
80scoped to one company
6blocked, each with a named reason
2correctly unscoped, permanently
Open work

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.

Live

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.

04 — Platform blindness

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.

Live

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.

Live

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.

Design rule

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.

Not built

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.

05 — Sign-in and sessions

There is no password to steal.

Nobody sets one, so nobody reuses one, and there is no password database to breach.

A link, not a password You ask for a sign-in link at your work address. It is a 256-bit random token, it works once, and it stops working 15 minutes after it is issued.
The reply never tells an attacker anything The response is identical whether or not that address is on a roster. Otherwise the sign-in form becomes a way to ask who works at your company.
The link is burned before anything else runs Single use is enforced on the stored row, and expiry is checked against that row rather than against anything the caller sends. If the step after it fails, a replay is already impossible.
Only hashes are stored The database holds a SHA-256 of the link token and of the session token, never the token itself. A copy of the database does not sign anyone in.
Sessions are bearer tokens 256-bit, 30 days, recorded with the device string and IP, last-seen timestamp updated as they are used, and revocable — signing out revokes immediately.
Rate limited at both ends A burst limit and a sustained limit per IP address, plus a cap of five links an hour for any one address, so neither the mailbox nor the database can be used as a weapon.
What is logged, honestly The audit trail records that a link was issued, not that the mail was delivered. A row saying a link was sent is not proof it arrived in the inbox.

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.

06 — Roles

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.

Owner Types the price the customer should pay and the engine works the discount backwards to it against the floor. Sees cost, margin and every concession. Grants and withdraws access. The last owner cannot be demoted.
Support The second pair of hands on the money surfaces: can change a proposal and release a concession above a rep's cap. The only other role that can write.
Operations Reads. A signed job arrives with its job sheet, hardware list and single-line diagram from the same quote, and can be opened exactly as the customer sees it. Cannot alter a price.
Accountant Reads the money surfaces — margin per job, what each concession cost, the signed ledger. Cannot change a proposal.
Rep Prices a job on site with the rebates already applied, and sees only jobs assigned to them. No cost base, no floor, no margin, no other rep's pipeline. A rep reads: every change to a quote goes through one desk, so the person selling a job cannot alter what was quoted after it was agreed.

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.

07 — The signed record

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.

08 — Retention and deletion

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.

Live

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.

Live

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.

Live

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.

Live

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.

09 — Leaving

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.

Partly built

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.

Live

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.

Not built

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.

10 — What we are not claiming

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.

No certification No SOC 2 Type 1 or 2, no ISO 27001, no IRAP assessment, no third-party penetration test report. Nothing is in progress that we can date.
No enterprise SSO No SAML, no SCIM, no directory sync. Access is a magic link to a work address and a roster your owner controls.
No platform support console The separate platform roles described in our own design notes are specified and not built. Today there is no Jouli-side account that can read your rows at all, which is stricter, not looser.
No signed uptime or support SLA Support is email, same-business-day on the Team plan and a named contact on Retailer. That is a commitment in the plan, not a credited availability guarantee.
No customer references There is one client, and it is the company that built the software. No logo wall, no case study, no testimonial. The 65 jobs are real and they are all from one operation.
Not an accredited installer Jouli is software. It holds no electrical licence, no SAA accreditation and no NETCC approval, and it is not a party to any homeowner's contract. Those belong to the installer using it — for client one, Mission Green.

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.