The Same Logic in the Browser and on the Server
A checkout page shows the total as you type: shipping, discount codes, tax. The server computes the total again before charging the card, because it can't trust the browser. That's the same logic written twice, often in two languages, and every change to shipping or tax has to land in both. When they drift, a customer sees one total and pays another.
Write it once, as a Stof document. The browser runs it through the npm package, the server runs it in Python (or Node.js, or Rust), and both get the same numbers, because it's the same document and the same runtime.
The Logic
Rates, discount codes, and the calculation, in one document. Press Run to price a cart:
/// Shipping, discount codes, and tax rates.
rates: {
free_shipping_over: 50
shipping: 7.5
tax: { US: 0, DE: 0.19, GB: 0.2 }
}
codes: { SAVE10: 0.1, FRIEND25: 0.25 }
/// The price of a cart: the same numbers in the browser and on the server.
fn quote(cart: map) -> map {
let subtotal = 0.0;
for (const item in cart.get('items')) subtotal += item.get('price') * item.get('quantity');
const discount = (subtotal * (self.codes.get(cart.get('code') ?? '') ?? 0)).round(2);
const discounted = subtotal - discount;
const shipping = discounted >= self.rates.free_shipping_over ? 0 : self.rates.shipping;
const tax = ((discounted + shipping) * (self.rates.tax.get(cart.get('country')) ?? 0)).round(2);
{
'subtotal': subtotal,
'discount': discount,
'shipping': shipping,
'tax': tax,
'total': (discounted + shipping + tax).round(2),
}
}
#[main]
fn main() {
const cart = {
'items': [{'sku': 'mug', 'price': 18, 'quantity': 2}, {'sku': 'tee', 'price': 25, 'quantity': 1}],
'code': 'SAVE10',
'country': 'DE',
};
pln(self.quote(cart));
}This example uses decimal amounts to stay readable. When exact money matters, keep amounts in cents, as integers, the way payment APIs do.
In the Browser
Load the published document once, and price the cart whenever it changes. Each quote takes a fraction of a millisecond, with no request to the server:
import { StofDoc } from '@formata/stof';
const response = await fetch('/checkout.bstf');
const checkout = await StofDoc.new(new Uint8Array(await response.arrayBuffer()), 'bstf');
async function updateTotals(cart: Cart) {
const quote = Object.fromEntries(await checkout.call('quote', cart) as Map<string, number>);
render(quote); // { subtotal: 61, discount: 6.1, shipping: 0, tax: 10.43, total: 65.33 }
}
With a bundler, initialize Stof first. See Browsers and Bundlers.
On the Server
The server loads the same document and computes the total it charges. Here it's Python, but Node.js and Rust work the same way:
from pystof import Doc
checkout = Doc()
checkout.binary_import(open('checkout.bstf', 'rb').read(), 'bstf')
@app.post('/orders')
def create_order(cart: dict):
quote = checkout.call('quote', [cart]) # a dict: the same numbers the browser showed
charge(cart['payment_method'], quote['total'])
return quote
A JavaScript object and a Python dict both arrive in Stof as a map, and the result comes back as a Map or a dict. The server never trusts the total from the browser. It doesn't have to, because it gets the same answer itself.
Publishing Changes
New tax rates and promo codes are a change to the document. Publish it as binary Stof, and the browser and the servers pick it up together, with no app release:
const doc = await StofDoc.new(checkoutSource);
await storage.put('checkout.bstf', doc.blobify('bstf'));
Add #[test] functions for the cases that matter ("free shipping at $50", "SAVE10 takes 10% off before tax"), and run stof test before publishing.
Why This Works
- Written once. One document is the logic for every place it runs, in any language Stof runs in.
- The same answer. One runtime, so no rounding differences or drift between implementations.
- Instant in the browser. Totals update locally, with the server as the source of truth.
- Changed without a release. Rates and codes ship as a document to both sides at once.
The same pattern fits form validation, quote and estimate calculators, eligibility checks, and anything else a user should see before the server decides.