Context
A till that is offline must still price a basket, apply promotions, compute tax, enforce discount limits and accept a return. Those rules are code that the pricing, promotions, tax and returns modules own on the server. Today a module’s code runs in its service, and a client reaches it only through the generated client. Copying the rules into the app would make two implementations that drift. The owner decided on 2026-10-24 that these rules are written once, as pure code, and run on both sides, with the server’s answer winning.Decision
-
Rule code is a package with a
coreentry. A module that must evaluate on clients puts its rule code in a package of its product, in the isomorphiccoreentry of 0294 decision 15, as pure functions infunctions/(<name>.function.ts). The module’s service and the client apps both import that package. There is no second copy. -
Pure means pure. A rule function takes its inputs as arguments and returns a result. It performs no I/O, holds
no state, reads no clock (the time is an argument, as 0331
decision 6 requires) and takes no injected service. It depends only on
coreandcontractsentries. A random number, a locale or a currency table is an input. - Data reaches the client as a replica. The tenant data a rule needs (price lists, promotion rule sets of 0331, tax registrations, return policies) is a bounded replica with an “as of” time (0338 decision 3), and the result names the replica’s revision and “as of” time it was computed from.
- The server re-evaluates and wins. Every document a client computes offline (a priced sale, a return) carries the rule code version and the replica revisions it used. On sync, the server runs the same function on its current data. An equal result is confirmed. A different result is recorded as a review item on the document, with both answers; it never rejects the sale, and the server’s figure is the one in the ledger.
- Versioned together. The rule package’s version is part of the client’s build, and an action that accepts an offline-computed document declares the oldest rule version it accepts without review. A document from an older version is re-evaluated on the server and flagged for review, never refused.
- Not tenant code. Only first-party and edition module code ships this way. A tenant’s rules remain data that references registered fields and operators (0331 decision 10); tenant logic runs only in the sandboxed code step (0154), which is server-side, and is therefore never evaluated offline.
Consequences
- One implementation of each rule, exercised by the same tests on both sides.
- A till can finish a sale without the network, and the shop sees any difference later as a review item with the two figures, never as a lost sale.
- A rule change is a package release, and the oldest accepted version is an explicit line in the action.
Enforcement
- planned — an oxlint house rule under 0129: a
corefunction that imports anodeentry, a driver, a service orDate.nowfails. - planned — a conformance test in
@stackra/testing: for each rule package, the same fixtures produce the same result in a Node run and in a jsdom run. - planned — gen (STA-340): an action that accepts offline documents without an oldest-accepted rule version fails.