Services
Why Qavlar Tech Stack Work About
Products
Blog Contact
AUTOMATION · SHOPIFY · LIVE IN PRODUCTION

Kassen-Dashboard: syncing a cash register to Shopify

For a cosmetics retailer I automated the stock sync between the in-store register and the online shop. The interesting part is less the feature list than what surfaced once it hit her real catalog and her real PC.

~285Real SKUs in the catalog
MinutesFrom PDF to Shopify stock
2Allow-listed emails, no password
0Secrets on the client PC
Kassen-Dashboard
The overview: last run, changed products, stock warnings and PDF upload. Product names and SKUs are blurred.

Two systems with no way of talking to each other

A cosmetics retailer ran bcassa, a German point-of-sale system used in-store, and Shopify as the online shop. Every in-store sale changed stock only inside bcassa. The owner's only way to reflect that on Shopify was to check a PDF inventory export and update products by hand.

Her existing tooling made it worse: a script that parsed that PDF by guessing which numbers were which, silently corrupting product names whenever a supplier name happened to contain digits.

What I built

An end-to-end sync system, plus a delivery mechanism built for a non-technical owner rather than a developer.

A real PDF parserColumn extraction uses each word's actual x/y coordinates and reconstructs columns from the header's own layout. Correct regardless of what is in each column, locked in with a regression test against the real export format.
A cost-aware Shopify syncWith ~285 SKUs and only a handful moving per day, a cache skips the Shopify API for anything unchanged. Before any real write it fetches the live quantity and uses Shopify's compare-and-set, so a concurrent online sale can never be clobbered.
A dashboard in the client's brandGerman, styled to the owner's own branding: recent runs, changed products, low-stock alerts, demand spikes, and best and worst sellers from Shopify's order data.
Passwordless authA one-time emailed link, allow-listed to two addresses, rate-limited, with long-lived sessions.
A self-installing Windows clientInstead of Google Drive (a security review neither side could clear), a background program watches a folder on her PC. Built by CI with no Windows machine, credentials baked in at build time from a GitHub Actions secret, it creates its own folder and auto-starts at login.
A deployment that can't go wrongOne script: git pull, Docker rebuild, restart, idempotent database migration. Every schema change ships automatically, no manual SQL, ever.

What actually broke

The differentiator of this project is not the feature list. It is what surfaced once it hit her real catalog and her real PC, and how each problem got run down.

A 504 on the first full-catalog upload. Not a code bug: the VPS's reverse proxy was timing out a legitimately long-running sync. Tracing it meant first discovering the server did not use a standard nginx layout at all. It was aaPanel-managed, with the real config nested under a panel-generated proxy directory that a grep on /etc/nginx never would have found. Only then could the fix be applied: a scoped timeout override in a file the panel's own UI will not overwrite.

Three Shopify Admin API contract breaks, back to back. A field that looked right in the docs (ignoreCompareQuantity) did not exist on the input type at all. The field that replaced it (changeFromQuantity) is a required compare-and-set precondition, not optional metadata. Then the mutation rejected every request for missing a newly mandated @idempotent directive. Each was diagnosed from the raw GraphQL error payload and Shopify's changelog. The fix for the second is itself defensive design: the sync fetches the live quantity right before writing instead of trusting a local cache, which also protects against clobbering a sale that happened in between.

A cache that could fail a product forever. A product got deleted and recreated in Shopify, a routine event, which gives it a new internal ID. The sync had no path back from "the cached ID no longer exists" other than erroring on that SKU on every future run. It now self-heals: a "not found" clears the stale cache and falls back to a fresh lookup by SKU, so one bad ID does not become a permanent, silent gap in the catalog.

Reporting success for a write that never happened. A failed Shopify write was, for a moment, still recording the intended new stock value locally before the write was confirmed. A transient failure could make the next run's cache check see a false match, skip the retry, and report "Erfolg" for a stock value Shopify never received. Fixed by only ever writing local state after a confirmed remote write.

Windows SmartScreen on a non-technical user's machine. The first handoff of the .exe hit the friction an unsigned binary predicts: she saw a blocking security prompt, read it as "the computer won't let me", and closed it. Diagnosing the dialog blind, from a two-line message in Hungarian and without being able to run the binary myself, meant writing the walkthrough for her language and her literal screen, not a generic troubleshooting doc.

Kassen-Dashboard
Log of recent updates with status, changed and skipped products.

Stack

FastAPIPostgreSQLSQLAlchemyDocker ComposeShopify Admin GraphQL APIMailjetPyInstallerGitHub Actionsnginx / aaPanelVanilla HTML, CSS, JS

Where it landed

Running unattended in production: she drops a PDF in a folder, stock updates on Shopify within minutes, and every failure, not just the ones she happens to notice, emails the developer automatically.

The parts that looked simple at spec time turned out to be mostly about the parts that are not simple in practice: a proxy layer neither of us configured by hand, an API that changed its contract three times mid-build, and software that has to be trustworthy enough for someone who will never read a stack trace.

Two systems that do not talk to each other?

Tell me in a free 30-minute call where things are still copied over by hand.

A project by Qavlar.tech, built in Saarbrücken.