www.meucaderno.digital Apr to Aug 2026 React · TypeScript · Postgres

Meu Caderno Digital

How I built a sales app where recording a sale fits in two taps, faster than scribbling on paper. And what I learned trying to prove it works.

Sales: 0
$ 0.00
Meat skewer $ 8.00
0

Tap +. Then confirm. That’s it.

The app's home screen, working right here

6+taps for a single sale in apps I tested
12competitors installed and analyzed
The origin

The street food stall

I was at a meat skewer stall on the sidewalk, and the guy was doing everything alone. Welcoming customers, writing orders, flipping skewers, charging, giving change, and in the middle of it all, scribbling into a greasy little notebook what had been sold. Five roles, one pair of hands.

The notebook was smeared with grease, with a bent wire binding. And it worked flawlessly.

I couldn't shake that observation. It's 2026. Since ancient Egypt, humans have recorded transactions on physical media. After all these millennia, and forty solid years of commercial software, street vendors still choose pen and paper. Not out of nostalgia. Out of raw efficiency.

That is either a mystery or a diagnosis. I wagered it was a diagnosis.

What research revealed

Before writing a single line of code, I did two things.

First, I listened. I visited street markets, sidewalks, pastry stalls, snack carts. I asked who takes notes, who doesn't, and the question that actually matters: who had tried taking notes on their phone and given up?

The pattern emerged immediately. Almost everyone who had tried had quit. And the reason was never “the app was badly programmed.” It was always a variation of the same sentence: “it’s too much work.”

Next, I downloaded the competition. A dozen of them. Installed them, signed up, and tried registering a single $8 meat skewer sale as if I were the street vendor facing a line of customers.

Not a single one allowed it in fewer than six taps.

Open → New sale → select customer → search product → choose quantity →
select payment method → choose tax category → confirm → return to home

None of those apps are bad software. Many are exceptionally well engineered. The problem is structural: they were designed for businesses with inventory, suppliers, accountants, and corporate tax IDs, and then sold to someone with a grill and a cooler. Every single field exists because someone, somewhere, needs it. But the sum of all fields everyone needs is a labyrinth for someone who needs two.

The true cost of an app isn't the subscription price. It's the number of decisions it demands per sale.

With a line forming and meat burning on the grill, six decisions cost more than a pencil scribble. So the scribble wins. It always has.

The notebook doesn't win by being great. It wins because the alternative is worse.

2taps, from finger to recorded sale
6features eliminated by the constraint
The decision

The criterion that became architecture

I left research with a strict number, not a feature list:

Recording a sale must fit in two taps.

One tap on the product. One tap on confirm. Done.

That sounds like a UI design goal. It isn't. It's an architectural constraint, and it kills features before they can ever be born. Look at what it eliminates:

  • No “new sale” screen. If you must open a sale before making a sale, you've already wasted a tap on bureaucracy. The home screen is the point of sale.
  • No search input. Searching requires typing; typing opens the keyboard; the keyboard consumes half the viewport. All products sit directly in a single visible list.
  • No quantity picker modal. There are simple [-] and [+] buttons beside each product, with the number in between. Three skewers mean three taps on +—the exact gesture of someone counting on their fingers.
  • No payment method picker. Cash, instant transfer, and card all flow into the same day's revenue. The vendor knows what came in, their notebook never asked, and gross revenue is identical in all three cases.
  • No register closing routine. The day flips automatically at midnight. The device clock already knows today’s date; asking the vendor to close the register is asking them to do the computer’s job.
  • No signup wall to get started. Open and use. An account is strictly optional.

Each absence was an explicit decision made against the intuition of “what if someone needs this?”. Yes, someone will need it. And they can use one of the other twelve apps that handle it well. This app does one thing.

12screens across the entire app
1list, zero search bars and zero categories
The interface

The screen

The main screen is a single list.

Sales screen with three selected products and total of R$ 26.50
The sales screen with an active order. Three green rows, the daily counter at the top, and the confirmation bar below. Everything else is pure list.

Each row displays the product name in monospace type, the price beneath, and on the right the three essential controls: [-], the quantity, [+]. The entire row shifts color as soon as quantity exceeds zero: light green surface, green border, green numeral. You perceive the full order at a glance without reading text.

When items exist in the cart, the bottom confirmation bar slides up with the total. One tap on it and the sale is recorded, the list resets to zero, and the header counter ticks up.

The list self-sorts by popularity. Best-selling items float straight to the top. Nobody hunts for an item; the item travels to the finger. If the vendor sells beef skewers, chicken skewers, and soda, and beef represents 60% of volume, it stays on row one by construction, not configuration.

This had an unforeseen side effect I had to solve: with 20 or 30 catalog items, popularity sorting scatters top sellers throughout alphabetical order, making rare items harder to find. The solution was not changing default sorting. It was an alphabetical sort toggle that represents transient state, not navigation. It sorts the list in place and resets upon reload.

The header tallies the day. Two lines: Sales: 12 above, $ 148.00 below in prominent type. It’s the metric vendors glance at between customers, always visible without opening any sub-menu.

The day closes itself

There is no shift closure button. Tapping the header counter opens the daily summary, and the day rolls over automatically when midnight passes. The history screen filters by hour, day, week, or month, and exports the selected timeframe in one tap directly into the native mobile share sheet. No bulky PDF reports nobody asked for.

Daily summary with seven items and total of R$ 61.00History with filters by hour, day, week and month
On the left, daily summary shown when tapping the counter. On the right, historical sales with period filters and the export button.
0network bytes to record a sale
Σ−Σcredit tab debt is never stored
Architecture

What happens when 4G drops

In street markets, 4G signal drops constantly. Inside metal canopy stalls, it drops even worse. A sales app that requires a network connection to log a transaction is an app guaranteed to fail on the busiest festival day.

Hence, the architecture is strictly local-first. This is not an implementation detail; it is the core promise. All operational state lives on the physical device. Sales are committed locally, instantly, offline. The remote server handles opportunistic synchronization: when connectivity is restored, pending mutations upload seamlessly.

It is not “the app displays what the server holds.” It is “the device holds the truth, and the server is a replica.”

The resulting complexity—what to do when multiple devices diverge—was pushed into data modeling rather than UI complexity. The clearest example is customer credit tabs.

The credit tab ledger

Selling on store credit (tabs) is the most common pattern in local commerce, and the most poorly addressed by software. A vendor hands over goods, receives no cash, writes “Joe: 2 skewers” on the notebook’s back page, and settles the balance on Friday.

The obvious temptation is creating an outstanding_balance column on the customer table, adding and subtracting. That is the wrong path because of synchronization: two devices belonging to the same vendor will conflict over that scalar field, silently losing money when writes clash.

The architecture chose an append-only model: debt balance is never stored. It is always derived.

  • Goods delivered without payment create a charge event, with item price frozen at delivery time.
  • Cash settled on Friday creates a settlement event.
  • Total outstanding debt is Σ charges − Σ settlements, computed on demand.

There is no scalar balance field. Therefore, there is no field for multiple devices to clash over. State converges because event logs merge monotonically, producing identical calculations on all sides.

Additionally, a crucial business decision: credit tab sales never inflate today's cash flow. Goods leaving are not recognized revenue until cash enters the register. Tuesday’s register shows actual cash collected on Tuesday, not promises. Exactly how the paper notebook operated, and the only version that never lies.

2customer paths to the order
1code, and nobody needs to speak it
Queue mode

When the vendor cooks to order

Meat skewers aren’t instant. Pastries take minutes in the fryer. An entire category of vendors struggles not with logging sales, but with remembering which customer owns which order on the grill.

For them, Queue Mode exists, and it deliberately inverts the architecture. Here an order requires a globally coordinated number across devices and must be readable by customers with no account and no installed app. Local-first cannot solve this alone. In this mode, and only in this mode, the server acts as the source of truth.

The vendor builds the order as usual and confirms; the ticket is assigned a sequential number. Orders render as visual cards progressing across states: preparing, ready, delivered, paid.

Two customer paths, and why they expose different scopes

This is among the most refined security designs in the product, born out of an audit discovery.

Order number modal with QR code and Paid / Unpaid toggles
Both customer journeys on one screen. The vendor flips the phone and the customer scans. Below in smaller type, the fallback: “No camera? Customer enters number 1 on the stall poster QR.”
Path 1

Vendor shows device screen

Shows full order details

Upon order creation, the app renders a unique QR code for that specific ticket. The customer points their camera. No spoken numbers, no manual typing, no mishearing over street noise. The QR contains the order number and a cryptographic token proving the scanner was physically at the counter: itemized list, quantities, total, payment status, and live prep status.

Path 2

Printed stall poster

Shows status only

A large, printed QR poster fixed to the stall banner, generated directly by the app. It links to the stall's public status page where customers type their ticket number. It displays live status (e.g. "Preparing" / "Ready"). It never exposes itemized contents or dollar amounts, prompting: “To view full items, ask for your order QR at the counter.”

This distinction is not a limitation. It is the entire security boundary.

Customer screen showing Order 1 preparing with items and totalCustomer screen showing Order 1 ready for pickup, paid
What the customer sees after scanning the order QR: items, total, and live status. On the right, the same ticket when marked ready for pickup.

The stall's URL is public by definition, printed on a poster where anyone can see it. If typing a simple integer like 1 or 2 yielded full receipt data, anyone seated nearby could iterate numbers and read everyone else's orders and spending.

The naive fix would be giving each ticket a spoken PIN (e.g. “Order 42, PIN 7391”). The customer would type both. In a loud, crowded market, that doubles verbal friction and doubles transcription errors.

The actual fix does the opposite: it eliminates cognitive friction on both sides.

The token still exists in full cryptographic strength, but nobody needs to speak or type it because it travels embedded within the dynamic QR code. The vendor turns the screen—far less effort than shouting numbers. The customer scans—far less effort than typing. And the printed poster remains a robust fallback for damaged cameras or tech-averse customers, showing only what is safe without physical counter proof.

Two honest notes on system design

QR binding is exclusive. Once an order is claimed by a customer device, it cannot be claimed by another; subsequent scans display an advisory to consult the vendor. This protects order ownership.

The poster path is non-exclusive, by deliberate design. Multiple companions at the same table can enter the same ticket number to watch for pickup alerts. Since only status is visible, zero private data is leaked.

Financial accounting remains strictly local. The moment the vendor marks a ticket as “Paid,” the app inserts local sale rows through the standard pipeline. Two architectures coexisting harmoniously where each makes sense, converging on a single clean interface.

Vendor queue card with progress action buttonsPrintable queue QR poster modal
On the left, the vendor’s live queue. The badge “Customer tracking via mobile” confirms someone is actively waiting for that ticket. On the right, the generated stall poster ready to print. QR and URL are blurred intentionally.
2catalog setup modes in the same dialog
1AI call, zero audio streams
1.77 msthe gate running before it
Day zero

My favorite engineering decision

The biggest roadblock in any point-of-sale app isn't day-to-day operation. It is initial catalog setup. Nobody wants to type 18 product names and prices into mobile form fields before selling their first skewer.

So the app offers two onboarding modes side-by-side as tabs:

Manual mode. Name and price, one product at a time. Predictable, works without internet, account, or AI, and remains available forever.

Natural text mode. A plain text area where vendors describe their menu naturally:

“beef skewer 8 dollars, soda 5, chicken skewer six dollars, mineral water 3...”

A server-side language model parses this natural description into a structured catalog, presenting an editable review table before committing to the database. Name and price, row by row, ready to adjust. AI suggests; the human decides.

Add products modal in Smart tab with unpunctuated dictated text
Dictated speech transcribed natively by the mobile keyboard and pasted without manual edits. Notice spoken numbers like “chicken skewer is eight dollars”: written words are normalized seamlessly by the deterministic gate.

Why this uses text, and never raw audio

Here is the architectural pattern I would take into any future product.

The standard, fashionable industry approach would be adding a prominent record button. The user speaks, the app uploads raw audio to a speech-to-text API, and a second model parses the resulting transcript. Two remote round-trips, two third-party bills, two failure points, and a hefty audio payload uploading over spotty 4G.

I built none of that. The input is a standard text area, and the microcopy above it reads:

Type your products and prices freely or tap your keyboard's 🎤 microphone icon to dictate.

Every mobile keyboard on iOS and Android already possesses a built-in dictation microphone. It transcribes audio on-device, for free, with native language models optimized for the user's voice and accent.

I outsourced the expensive computational tier to software already running on billions of devices, retaining only what is genuinely my problem: interpreting structured text.

The architectural benefits compound across four fronts simultaneously:

  1. Cost. One text inference call instead of audio token pipelines.
  2. Bandwidth. Uploading kilobytes of text instead of megabytes of audio. On weak cell towers, this is the difference between working and failing.
  3. Privacy. Voice audio never leaves the vendor's device.
  4. Code surface. Zero audio recorders, zero media format codecs, zero microphone permissions to request, zero interrupted audio streams to recover. Code I didn't write is code I never have to debug.

And the user experience is unified: whether a user prefers speaking or typing, it’s the exact same text field. There are no divergent flows.

The gate running before AI

There is another essential piece: before spending an API token, a deterministic regular expression inspects whether the input actually resembles a menu—checking for word tokens paired with numeric values (either digits or written-out number words).

It understands written number words in Portuguese from “um” to “novecentos”, including colloquialisms like “meia” (half/six) and dialect spellings. Because speakers dictate words while typers use digits, and both must succeed.

If the input fails this gate, the UI immediately displays actionable guidance on what is missing, with zero network latency. Before this guard, the app would fail late with generic errors like “AI failed to process text,” which was inaccurate since the AI hadn't even executed.

Nine products extracted by AI, each editable before savingProducts active on sales list with names and prices
Nine items recognized, all editable before saving. On the right, the active menu populated instantly on the point-of-sale screen.

Two benchmarks were measured and codified in tests:

  • Worst-case regex input benchmark: 1.77 ms on 1,000 characters. Safe because 1,000 chars is an enforced upper ceiling on both client and server.
  • The gate deliberately errs on the permissive side. Borderline queries pass through to inference, preventing false-negative rejections of legitimate menus.
1.75:1contrast of logo green on white
4.5:1accessible minimum
2blues, because one could not fit
+5.6 pxsame font, different layout widths
The design

Color measured, not guessed

Here is where the project became something else entirely.

The app features complete light and dark themes backed by design tokens. Components never reference raw color literals; they reference semantic roles: surface, fg, success, danger, warning. Theme switching involves zero conditional branches in component code.

That is good practice. What followed went further: I stopped picking colors and started calculating them.

The sales screen in light themeThe same sales screen in dark theme
The same screen in both themes. Zero components know color values; all know semantic roles.

The vibrant brand green is #00E175. On pure white, it achieves only 1.75:1 contrast, far below the 4.5:1 WCAG AA threshold. Consequently, that green is never used as text. It serves as background fills; foreground text uses a darkened, high-contrast hue. (This case study applies the same principle: green appears across borders and accents, never as text.)

Similarly, brand blue could not serve as the universal button blue for geometric arithmetic reasons. For white text to pass AA contrast over a tinted surface, the tint must be sufficiently light; for the tint to stand out against the page background, it must be sufficiently dark. Their intersection is mathematically empty. So two distinct blues were formulated:

#004DFF#2466FF
white text on top5.994.75
against light background4.973.94
against dark background3.103.90
against dark modal panel2.032.55

The deeper blue powers the header banner surface. The lighter blue powers interactive buttons, optimizing contrast across both dark and light modes.

Buttons are modeled as tactile keys: solid surfaces with a darker bottom edge providing physical relief. The formula is a single rule:

The bottom bevel edge is the surface color multiplied by ⅔ across channels in the exact same hue.

An edge from another family reads as two glued pieces, not a key. And what remains unified between buttons is the relief ratio (~2.20:1), not the multiplier: the same ⅔ yields 2.20:1 on green and only 1.70:1 on blue, because the blue face starts from lower luminance.

A decision lost to mathematical precision

I originally wanted a unique accent color for credit tabs. When measured, every amber shade passing AA contrast across both themes landed within 8 to 15 degrees of the existing warning yellow—too close for human visual distinction.

There is no separate credit tab yellow. Conceptually, pending credit tabs and warning states represent the same fundamental domain state: an unresolved balance.

Every rule runs in CI as automated tests. Test runners inspect generated CSS and fail the build if any hue deviates from its palette family or fails WCAG AA contrast against its host surface. It is an automated gate, not aspirational documentation.

It exists because the manual alternative failed: a surface once shipped in hue 43 (brown) in dark theme while the rest of the palette lived between 213 and 222 (slate blue), tinting the product card with a foreign color. It passed all checks. tsc does not parse string colors. eslint does not parse string colors. The build generated the rule without complaint.

Typography and cross-platform rasterization

The app self-hosts two variable fonts: Google Sans Flex for interface UI and Geist Mono for product names and numeric figures. Self-hosting guarantees that street vendors never suffer FOIT/FOUT or third-party downtime at the market.

When font files were integrated, two geometric visual regression tests consistently failed in Linux CI while passing locally on macOS. The fonts had loaded, but layout bounding boxes diverged:

                       local macOS     Linux CI server
text content width        105.375         105.375   ← identical (font loaded)
button wrapper width      169.172         174.766   ← +5.6px divergence

Linux FreeType applies different glyph outline hinting than macOS CoreText, causing the same font file to yield distinct pixel widths under flex containers.

Identical font files do not guarantee identical bounding box metrics across operating systems.

The solution was querying unhinted geometric layout bounds explicitly at the root level, standardizing cross-platform metrics to 69.375px exactly.

Other details that became automated tests

  • All text inputs are at least 16px. Below 16px, iOS Safari auto-zooms on focus, causing viewport layout jumps. An automated AST scanner fails the build on any smaller input font size.
  • Every dialog is a real dialog: announces itself to screen readers, closes on Escape, traps focus inside, and locks background body scrolling.
  • Accessible name matches visible label (WCAG 2.5.3). For speech-control users, spoken commands match button text exactly.
  • Browser confirm() alerts were eliminated across all instances and replaced with native accessible modal dialogs.

The sound that never lies

The app plays an audio chime when a sale is confirmed. It sounds simple. It is governed by the strictest rule in the codebase:

Sound confirms the outcome, never the tap.

Out of six audio triggers, four involve network mutations that can fail. If audio fired immediately inside the onClick handler, a vendor under spotty 4G would hear the success chime, the network write would fail, and they would proceed to the next customer. The app would have lied to them in the single moment they trusted it without looking at the screen.

Therefore, sound triggers strictly after the await resolves in the success branch. An automated static analysis test verifies that zero audio calls exist inside direct UI event handlers.

There is a crucial corollary: when a vendor double-taps confirm in haste, the second tap plays zero sound. No second sale took place. Playing a confirmation would be a false affirmation.

9defects the old suite missed
502automated tests today
14 sfull verification duration
85 : 82test files to code files
Verification

How I know it works

This is the part I take the most pride in, where the project evolved into an engineering discipline.

I started with standard tooling: TypeScript, ESLint, unit tests. Everything passed green. Yet during manual regression testing, I discovered nine distinct behavioral bugs—none of which were caught by the existing suite. All nine were already live in production.

The diagnosis was uncomfortable and clear: I had multiple layers reading code structure, and zero layers executing runtime behavior. Structural verification catches structural syntax errors. The entire class of sequential defects—state transitions firing out of order, lost focus traps, conditionally unrendered branches—passes through linters and type checkers without touching anything.

Today the product is guarded by four distinct testing tiers:

TierWhat It Proves
Pure logic and code structureContract correctness; file declares X and caller invokes Y
Runtime (real DOM)State transitions, focus movement, tree commits
Geometry (Chromium)Panel transform contexts, layout shifts, container bounds
Database (real SQL)Row-level security enforcement under standard user roles

The full test suite executes in under 14 seconds. A verification suite that takes two minutes is a suite developers stop running.

# tier 1: logic and code structure
$ npm test
ℹ tests 392
ℹ pass 392
ℹ fail 0
ℹ duration_ms 1271.589542

# tier 2: behavior in real DOM
$ npm run test:runtime
ℹ tests 110
ℹ pass 110
ℹ fail 0
ℹ duration_ms 3035.397875

# tier 3: geometry in Chromium
$ npm run test:visual
  41 passed (48.0s)

# automated verification run per turn
$ time npm run verify
real 14.59
user 40.61
sys   4.29
The four lines, executed on 27/08/2026. 502 tests across the first two tiers, 41 in the third, and full verification in 14.59 s. Zero figures in this article were estimated.

It runs automatically. A git hook triggers verification at the end of every turn where code files changed, blocking commit completion on failure. Written guidelines ask; automated hooks enforce.

The example justifying the geometry tier

I almost skipped building the geometry test tier. Booting real Chromium instances to test CSS felt expensive. Here is the case that convinced me:

There was a CSS rule on modal panel containers: transform: none. Change it to transform: scale(1).

That passes TypeScript. Passes ESLint. Passes both runtime DOM test tiers. Passes Tailwind compilation. And looks invisible in code review because matrix(1, 0, 0, 1, 0, 0) is identity—the element moves zero pixels.

Except any CSS transform other than none forces the element to become the containing block for all fixed-position descendants. The date picker calendar inside the checkout modal, which should occupy the full viewport width, shrank to the width of the modal panel.

Expected width: 1280  ·  Computed width: 384

No static code review catches this. Only a real browser layout engine knows.

The rule that changed how I write tests

Every bug fix gets a test. That is standard. What is essential is the second half:

Write the test. Revert the fix. Watch the test turn red for the exact right reason. Restore the fix.

Without this step, you cannot prove whether the test actually guards the defect or merely runs in its vicinity. A test that was never seen failing is worse than no test—it consumes trust without delivering verification.

5 → 3places answering, three answers
$ 22recorded while screen showed $ 10
The finding

My favorite discovered bug

A simple question: how much is the active shopping cart worth right now?

Five different modules across the app calculated this. Under a specific race condition—when an item was deleted from the catalog while sitting in an active cart—they returned three conflicting answers simultaneously.

Cart value: $ 22 → UI displayed: $ 10 → Database recorded: $ 22
With discount: Vendor charged $ 2 → Register recorded: $ 14

This is the type of defect that fits in zero git diffs. None of the five files was incorrect in isolation; the flaw existed in the contradiction between them.

The solution was establishing a domain invariant: “Every item in the cart must exist in the active catalog.” Deleting a product automatically clears it from active carts, making all five readers correct by construction.

Every core business question must have a single named owner. When five components answer independently, divergence is a certainty.
3 → 2objects deciding identity, two criteria
8defects in the fix itself
0found by code reading
Security

Without blueprints

The application implements defense-in-depth: Content Security Policy without inline script execution, strict HTTPS transport, Row-Level Security on all database tables, anonymous visitor lockdown, and append-only audit trails on administrative actions.

Three database objects answered “which account owns this session?”, with two differing criteria.

A previous audit had resolved two of them. The third was the exact routine writing the binding. A single canonical authorization function was established, backed by real integration test suites verifying that unauthorized mutations fail closed.

Reviewing the security fix itself

After implementing the fix, I audited the repair. It contained eight defects of its own.

And here is the takeaway: zero of the eight were found by reading code. All were surfaced through automated execution and adversarial platform tests.

72documents, ~45k lines of prose
13recorded audits
The process

Documentation that doesn't rot

The project contains 72 documents and roughly 45,000 lines of prose: design specs, implementation plans, audits. And one governing rule:

No expiring fact is written as static text. The command that rediscovers it is recorded instead.

Documentation is permanent; code evolves. Static assertions decay over time. Writing the rediscovery command (the SQL query, the grep pattern) converts the documentation decay rate to zero.

524commits in four months
210 KBcompressed over the wire
The balance

What the numbers reveal

14,055lines of code in 82 files
12screens
73database migrations
502automated tests passing
41Chromium geometry tests
14SQL invariant assertions
~14 sfull verification suite
~210 KBgzipped JS + CSS bundle

The final metric completes the loop: 210 KB. Smaller than a single smartphone photo. An app designed for low-cost hardware over weak cell connectivity cannot afford a 4MB payload.

Key takeaways

  1. The right constraint eliminates more bloat than any roadmap. “Two taps” was not a vanity goal; it was an active filter against unnecessary complexity.
  2. The best AI solution is often the one that uses the least AI. Leveraging built-in mobile keyboard dictation delivered better speed, zero API cost, and total user privacy.
  3. Measurement outperforms guesswork. Mathematical contrast modeling produced a cleaner, more accessible interface than subjective color picking.
  4. Reading code is not verifying code. Rigorous automated test execution is the only reliable proof of runtime behavior.
  5. Every core question needs a named owner. Divergent calculations across multiple modules cost real money and user trust.
  6. Green test results must be trustworthy. False-passing tests and decaying docs erode project velocity.
  7. Paper is a formidable competitor. Physical notebooks have zero boot time, require zero battery, and never disconnect. Software must compete on reduced friction, not bloated features.

The sidewalk street vendor still uses his notebook, and that is completely fine. I didn't build this to force a change. I built it to discover whether software could match the speed of paper. It can. Two finger taps, automatic shift balancing, and complete offline resilience.

The rest—the 502 tests, 73 migrations, and measured color tokens—exist so those two taps never fail.

Back to site