Notes from the build.

What shipped, what licensees told us, and where the system is headed.

  1. ComplianceOperations and compliance, every page in the demoEvery page, dialog and write of the first three phases now works in the demo agency. What that covers, area by area, and what comes after it.
  2. PlatformWhy the demo moved into the browserIn two days the demo agency moved twice: first into a branch inside every service, then out of the server altogether. What we got wrong the first time, and why the second answer holds.
  3. ProductThe property is the fileWhy Briesa files the transaction under the property, and every other system does it the other way round.
  4. TrustReconciled by the month, not by the miracleWhat we learned building sales and rentals trust accounting to the NSW rules, and why the audit pack should be an export.

Operations and compliance, every page in the demo

Compliance

Every page, dialog and write of the first three phases now works in the demo agency. What that covers, area by area, and what comes after it.

Topics

6 posts

All posts
  • Compliance1

    The policies, checklists, registers and audit pack an agency keeps, and the releases that built them.

  • Platform1

    How Briesa is built underneath: the workspace, the demo and the decisions behind them.

  • Product1

    How the system is modelled, and why the property is the file.

  • Trust1

    Sales and rentals trust accounting, built to the NSW rules.

  • Company1

    Why we build the way we do, and why New South Wales comes first.

  • Pricing1

    How Briesa is priced, and why it is priced per agency rather than per seat.

Documentation

Briesa Docs

Setting the agency up, deciding who sees what, and bringing your properties across.

Read the docs
Engineering notes

Why the demo moved into the browser

Published 26 September 2026By The Briesa team

In two days the demo agency moved twice: first into a branch inside every service, then out of the server altogether. What we got wrong the first time, and why the second answer holds.

On this page

Briesa has a demo agency anyone can open. It lives at its own address, agency. on our domain, and it is one example agency, opened by whoever arrives, with no account behind it.

Between 23 and 24 September 2026 we changed where that demo lives twice. This is what the demo was, why our first answer was the wrong one, and the rules we build to now.

What the demo was

The workspace was built the way the roadmap asked: a full UI over the Demo Agency’s seed data first, with the database wired underneath it afterwards. On the server that meant a parallel data layer, src/apps/app/src/server/mock/, which every page imported instead of the API.

A parallel data layer is a second product to keep in step with the first, and it was never going to converge on its own. It also leaked in both directions.

The calls the chrome made from the browser in the demo reached real endpoints against a real database. Signing out of the demo ended a session that was not yours; changing your name in it wrote to somebody’s account. And the server borrowed the seed’s licensee for anybody the mock had never heard of, so the screens had a shape to draw. That is how the organisation switcher came to offer a new agency’s owner a row for Demo Agency.

Timeline

The two days, from the commits and the rules they recorded:

    1. The chrome stops calling out

      The demo’s own calls are answered in the browser, and the sandbox sends no requests.

    2. Rule 38: a branch in the service

      The mock layer moves beside its modules, and a write outside the sandbox is refused.

    1. Rule 39: the demo is the client’s

      Recorded that morning, keeping rule 38’s question and replacing its answer.

    2. The list of what is owed

      Screens fetch the API; what has no endpoint yet is a .server.ts file. Sixteen by the afternoon.

    3. The last one goes

      The last server action files go, then the server-side demo with them.

    1. Merged

      The demo moves into the browser, and every provider gains a local stand-in.

Figure. How the demo moved, 23 to 25 September 2026.

A branch in the service

On 23 September we deleted the mock layer as a layer. The question it had been dodging, whether a request is the sandbox or a real person in a real organisation, would be asked inside each module’s service. It was answered by RequestContext.sandbox, which the workspace already computed from the host by the same hostKindOf rule the proxy and the shell decide with. The fixtures moved beside the modules they served, one <module>.mock.ts each: thirty-seven of them.

The host decided and nothing else. agency. and builder. were the sandbox by label, never by slug, so an agency that happens to be reached at demo-agency. is an organisation like any other.

Reads were safe by construction: every one filters by the organisation’s token, and a real organisation’s token is in no seed. Writes were not. The demo’s dataset lived in the server’s process and was shared by every request it handled, so a real organisation’s write that reached it would push a row tagged with that organisation into an array everybody could see until the process restarted. So every write began with a guard:

sandbox.store.ts, 23 September, since deleted
export const sandboxOnly = (ctx: RequestContext, action: string): void => {
  if (!ctx.sandbox) throw new NotWiredError(action);
};

It had to be the first line of the write and not the audit entry underneath it, because by the time the audit line ran, the row was already pushed.

The wrong half of the building

The guard worked. The shape did not. A service that asks whether it is the demo is a service carrying a second implementation of the product: two code paths to keep in step for every domain, forever. That is the thing the mock layer had been deleted for.

The question was right and the place was wrong. The demo is one visitor’s agency, changed by them and seen by nobody else, so it lives in the browser, and the browser is where the question should be asked, once.

The demo is the client’s

The next morning we recorded rule 39. There is no ctx.sandbox, no *.mock.ts and no sandboxOnly anywhere under the apps’ servers: a module reads and writes the database, and that is the whole of it. The branch is one line in the API client every screen already calls through, and the host still decides:

src/packages/elements/hooks/use-sandbox/index.ts
export const inSandbox = (): boolean => {
  if (typeof window === 'undefined') return false;
  const label = window.location.host.split(':')[0]?.split('.')[0] ?? '';
  return (PRODUCTS as readonly string[]).includes(label);
};

On the server there is no window and the answer is false. That is not a gap: nothing on the server calls this client. In the client, the check comes before anything else:

src/packages/elements/hooks/use-api/client.ts, trimmed
const send = async <T>(
  method: Method,
  path: string,
  body?: unknown,
): Promise<T> => {
  if (sandbox()) return (await sandboxRespond(method, path, body)) as T;

  const headers = new Headers({ accept: 'application/json' });
  // …
  response = await doFetch(`${options.baseUrl ?? apiBaseUrl}${path}`, {
    method,
    headers,
    credentials: 'include',
    // …
  });

It is before the headers, before the URL and before anything that could throw, so there is no path through the function that opens a socket in the sandbox. Not sent and ignored, not sent to a stub: nothing is sent. That is the property worth having, because it is the one somebody can check.

And the sandbox remembers. The point of a demo is that you can change something and see it changed, so its answers are kept in localStorage, per browser and per origin. They survive a reload and are thrown away by clearing site data, which is exactly the life a demo should have.

src/packages/elements/hooks/use-sandbox/index.ts, trimmed
const STORE_KEY = 'briesa.sandbox';

const read = (): Store => {
  try {
    const raw = window.localStorage.getItem(STORE_KEY);
    if (raw === null) return {};
    const parsed: unknown = JSON.parse(raw);
    return typeof parsed === 'object' && parsed !== null
      ? (parsed as Store)
      : {};
  } catch {
    return {};
  }
};

Every read and write is wrapped, because localStorage throws in a private window or with site data blocked, and a demo that fails to open because storage is off is worse than one that forgets. What the sandbox has no answer for, it refuses: a call it cannot answer throws SandboxUnsupported rather than falling back to the network. A silent fall-through would make “no requests are sent” true of the calls somebody remembered and false of the next one added.

What it cost

A page now fetches; it does not import a service. The agency screens read /api/v1/* through the use-api hook, which means every domain owes REST endpoints: a controller, a service, a module, its DTOs and adapters, and a route.ts that names the verb. The price is the server rendering those pages had. What it buys is one product instead of two.

A screen could not wait for its whole domain. Most of a domain’s writes had routes and a few did not, and holding a screen back until the last one landed would have meant converting nothing. So a screen’s actions/<domain>.ts fetched, and the writes still reaching into the server moved beside it as actions/<domain>.server.ts. The set of those files was the list of what was still owed, readable at a glance instead of inferred.

The list grew before it shrank. There were two such files at midday on 24 September and sixteen by the middle of the afternoon, as screens converted faster than endpoints landed. By the evening there were none. A feature still waiting on a provider got an endpoint that answers 501 with the reason until one is chosen, and the demo answers it in the browser.

What holds now

The rules we build to from here, as recorded:

  • The server knows nothing about the demo. No sandbox flag on the request, no mock files and no guard on the writes.
  • The host decides. The product’s own hosts are the sandbox, by label and never by slug.
  • Nothing is sent. The client answers the sandbox before it builds a request, and refuses a call it has no answer for.
  • A real organisation sees empty until its module lands. A new agency has no properties and no tasks yet, and an empty list is the honest answer. The demo’s rows would be somebody else’s data wearing the signed-in person’s name.
  • Every provider has a local stand-in. Chosen by the absence of its key, so a laptop never calls a real one.

You can check the first of these yourself: open the demo, change something, and look in the network tab for the request that saved it. There isn’t one.

One file per property, for its whole life.