Engineering note SempreVenda 2026
Why I rebuilt SempreVenda around a Rust transaction kernel
The first version of SempreVenda ran inside the bar and café I operated. It taught me what service feels like from behind the counter. It also showed me how quickly money, stock, printing, and reports can drift apart when each part of an application owns a different piece of the rules. I stopped adding features and rebuilt that foundation. This is what actually exists in the repository today, including the parts that are still unfinished.
What the first version taught me
A hospitality system has to do more than take an order. During one service it may need to open and move tables, send items to different production stations, split a bill, accept cash and card, calculate change, track a drawer, adjust stock, and leave enough evidence for someone to close the night without guessing.
The first version covered a large part of that surface. More importantly, it put me close enough to the work to see where the real risk was. A slow screen is annoying. A payment that appears twice, a drawer that cannot be reconciled, or a report that disagrees with the sale is a different category of problem.
Why I stopped patching it
The old architecture let business decisions happen in several places. The browser could shape a sale, another path could price it, payment code could interpret the balance, and reporting code could rebuild its own version of the result later. A local fix could solve the visible bug while adding one more definition of the same transaction.
I reached the point where adding features was easier than proving they were correct. That is a bad trade for software that handles somebody else’s money. I paused the product work and started again from the transaction boundary.
What is in the repository now
The center of the new system is sv-core, a Rust crate that makes deterministic business decisions. It does not open the database, read the clock, call a provider, or print anything. It receives typed state, a typed command, and a context that already contains the time, operating day, policy, actor, device, and pre-generated identifiers it is allowed to use.
A second Rust crate, stationd, owns the local Station process. It authenticates the caller, reads and writes SQLite, runs every mutation through one processor, exposes the LAN API, and dispatches work that must happen outside the transaction. The browser application is React and Vite. It talks through a generated TypeScript contract instead of building request shapes by hand.
The command surface is much larger than this excerpt, but the shape below comes directly from the code:
// recorte real do enum
enum Command {
OpenSale(OpenSaleCmd),
ChangeSaleLines(ChangeSaleLinesCmd),
RecordPayment(RecordPaymentCmd),
SettleSale(SettleSaleCmd),
OpenCashSession(OpenCashSessionCmd),
CloseCashSession(CloseCashSessionCmd),
AdjustStock(AdjustStockCmd),
RequestReprint(RequestReprintCmd),
}
// ja chega dentro do contexto
fn decide(
state: &AggState,
command: &Command,
ctx: &Ctx,
) -> Result<Decision, DomainError>
The command ID is not hidden inside that enum. It lives in the command envelope with the device, contract version, expected aggregate versions, payload, and offline metadata. That distinction matters because the Station fingerprints the meaning of the whole envelope. Reusing an ID with the same fingerprint returns the stored receipt. Reusing it for different work is rejected as a replay mismatch.
One path for every write
Every mutation passes through the same processor. It authenticates the caller, checks for an existing receipt, authorizes the command, captures the operating context, opens a SQLite BEGIN IMMEDIATE transaction, loads the declared aggregate state, checks expected versions, and calls the pure decision function.
If the decision is accepted, the Station applies state changes, journal events, immutable artifacts, projection changes, external effects, and the receipt in a fixed order inside that transaction. Only after SQLite commits does it wake the effect workers and answer the caller. A deterministic business rejection also gets a receipt, because that answer is final for the command ID. Authentication failures, version conflicts, and transient I/O failures do not get receipts, because a changed condition or a retry may produce a different answer.
This is more precise than saying that every error is final or every retry is safe. The code has an explicit error taxonomy because the offline queue needs to know the difference.
Retry and crash behavior
A terminal can lose Wi-Fi, a cashier can tap twice, and a process can disappear after SQLite commits but before the browser receives the response. The client creates the command ID when the operator expresses the intent. If the answer is lost, it can submit that same command again. The Station either returns the existing receipt or decides it once.
The receipt, journal entries, state changes, and queued effects share one database transaction. If the process dies before commit, SQLite rolls the transaction back. If it dies after commit, the receipt is already there. The repository tests both injected crash points and real process termination during a write. That is the behavior I care about: no state where half a payment survived and somebody has to repair it by hand.
Offline without inventing a second authority
The browser has an IndexedDB command journal. It stores commands, not patches to business state. It records the intent before trying to send it, preserves ordering for commands that touch the same aggregate, and keeps rejected or blocked work visible instead of silently dropping it.
The browser does not run a second copy of the kernel while disconnected. When the Station is reachable again, it resubmits the original command with the original ID and the offline metadata captured when it was queued. The Station remains the place that decides. This costs some offline immediacy, but it avoids a much worse problem: two authorities accepting different versions of the same sale.
Printing and providers happen after the decision
Printing, fiscal submission, provider calls, and relay work cannot run inside the database transaction. They are represented as effects in an outbox. Effects are ordered by lane, leased to workers, retried when appropriate, and deduplicated by an idempotency key.
This does not promise that a printer or payment provider behaves exactly once. External systems rarely make that promise. It means the Station records one durable intention, links every attempt to its cause, and makes the final outcome visible. The core decides that work is required. An adapter performs it later and reports back through another typed command.
The rules are written down
The repository has twelve signed domain laws written in Brazilian Portuguese. They cover things an operator can check, such as conservation of a settled bill, exact bill splitting, one recorded effect for money, cash custody, settlements, cashback, suspense, printing, and fiscal evidence.
Those laws are connected to named tests in an evidence register. The Rust core also has a replay corpus that checks deterministic decisions byte for byte. CI gates reject floating-point money, SQL outside the Station store, a second pricing implementation in the web application, raw network calls from feature code, and unreviewed panic paths.
Pricing deserves its own detail. The authoritative calculation lives in Rust and uses integer cent values. A small WebAssembly facade exposes the same pricing rules to the browser for provisional quotes. The preview identifies itself as non-authoritative, and a rules-version mismatch makes pricing unavailable instead of falling back to fresh JavaScript arithmetic.
What the audit trail really is
The whole database is not append-only, and I do not want to claim that it is. Operational aggregate rows change as a sale or cash session moves through its state machine. The evidence spine is the protected part: command receipts and journal events are immutable, while payments, cash movements, revisions, and other sensitive facts use append-only records where the model requires them.
Corrections happen through new commands. They do not rewrite the receipt that proved what the system accepted earlier. Older evidence can move out of the live database into a sealed archive, but the Station keeps enough registry information to answer whether an archived command was accepted or rejected. That detail exists to prevent an old payment from becoming “not found” and being charged again.
- 01Deterministic coreBusiness decisions run without database, network, filesystem, clock, or random access.
- 02One write pathA serialized Station writer commits state, evidence, effects, and the receipt together.
- 03Receipts with meaningAccepted and deterministic domain outcomes are durable. Retryable failures remain unreceipted.
- 04Commands, not patchesThe offline journal preserves intent and lets the Station remain the authority.
- 05Effects after commitPrinters, providers, fiscal adapters, and relays never hold the money transaction open.
- 06Written lawsOperator-readable invariants are tied to concrete tests and replay evidence.
- 07Visible gapsUnfinished journeys stay blocked in the repository instead of being described as complete.
Where the interface sits now
The web application is allowed to orchestrate and present. It can collect input, submit commands, format money, sort rows, and show pending work. It is not allowed to decide the amount due, whether a payment settles a sale, what is refundable, what the cash drawer should contain, or what stock and cost facts mean.
That boundary is enforced in code, not just described in a diagram. Feature modules cannot call the Station with raw fetch. They go through the platform layer and generated client. The same rule keeps a waiter screen, cash screen, kitchen board, and reporting view from growing their own competing versions of the business.
What is still left
The Rust kernel reached its first tagged milestone, and the repository closed the gate that allowed product migration to begin. That is not the same thing as saying SempreVenda is finished or ready for an unattended production rollout.
Many user journeys now have real UI and tests, but the release ledger still records blocked or unverified work around clean-machine setup, printer operations, fiscal outcomes, backups and restores, provider custody, and drills on real hardware. Those are not footnotes. They are the work between a convincing architecture and a system I can responsibly put in somebody else’s venue.
The next phase is less glamorous than starting another rewrite. It is closing those gaps, running the failure drills, and replacing every design claim with evidence from the actual Station.
Agnaldo Guerra Southern Brazil 2026